diff --git a/GuruConsent-Android/.gitignore b/GuruConsent-Android/.gitignore new file mode 100644 index 0000000..bb2a2bd --- /dev/null +++ b/GuruConsent-Android/.gitignore @@ -0,0 +1,14 @@ +.gradle +/captures +/local.properties +/.idea/workspace.xml +.DS_Store +/build +.idea/ +*iml +*.iml +*/build +/lib +wh.properties +/atom +*.txt \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/.gitignore b/GuruConsent-Android/GuruConsent/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/GuruConsent-Android/GuruConsent/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/build.gradle b/GuruConsent-Android/GuruConsent/build.gradle new file mode 100644 index 0000000..3cee5e4 --- /dev/null +++ b/GuruConsent-Android/GuruConsent/build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'maven-publish' +} + +apply from: 'maven-publish.gradle' + + +android { + compileSdk 33 + + defaultConfig { + minSdk 21 + targetSdk 33 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + implementation 'com.google.android.ump:user-messaging-platform:2.1.0' +} \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/consumer-rules.pro b/GuruConsent-Android/GuruConsent/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/GuruConsent-Android/GuruConsent/maven-publish.gradle b/GuruConsent-Android/GuruConsent/maven-publish.gradle new file mode 100644 index 0000000..4930998 --- /dev/null +++ b/GuruConsent-Android/GuruConsent/maven-publish.gradle @@ -0,0 +1,69 @@ +publishing { // Repositories *to* which Gradle can publish artifacts + repositories { RepositoryHandler handler -> + handler.mavenLocal() + maven { + url "$buildDir/repo" // change to point to your repo, e.g. http://my.org/repo + } + } + publications { PublicationContainer publication -> + + // Creates a Maven publication called "myPublication". + maven(MavenPublication) { + groupId 'guru.core.consent' + artifactId 'GuruConsent' + version '1.1.0' // Your package version +// artifact publishArtifact //Example: *./target/myJavaClasses.jar* +// artifact "build/outputs/aar/aar-test-release.aar"//aar包的目录 + afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) } // 方式一:生成aar包 + + //带上依赖 ,否则会报错 + pom.withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + def scopes = [] + if (configurations.hasProperty("api")) { + scopes.add(configurations.api) + } + if (configurations.hasProperty("implementation")) { + scopes.add(configurations.implementation) + } + if (configurations.hasProperty("debugImplementation")) { + scopes.add(configurations.debugImplementation) + } + if (configurations.hasProperty("releaseImplementation")) { + scopes.add(configurations.releaseImplementation) + } + +// if (project.ext.targetType != "jar") { +// scopes.add(configurations.provided) +// } + + scopes.each { scope -> + scope.allDependencies.each { + if (it instanceof ModuleDependency) { + boolean isTransitive = ((ModuleDependency) it).transitive + if (!isTransitive) { + println "<<<< not transitive dependency: [${it.group}, ${it.name}, ${it.version}]" + return + } + } + + if (it.group == "${project.rootProject.name}.libs" || it.version == 'unspecified') { + return + } + + if (it.group && it.name && it.version) { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + dependencyNode.appendNode('scope', scope.name) + } + } + } + } + } + } + + +} \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/proguard-rules.pro b/GuruConsent-Android/GuruConsent/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/GuruConsent-Android/GuruConsent/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/src/androidTest/java/guru/core/consent/ExampleInstrumentedTest.kt b/GuruConsent-Android/GuruConsent/src/androidTest/java/guru/core/consent/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..cfe8da4 --- /dev/null +++ b/GuruConsent-Android/GuruConsent/src/androidTest/java/guru/core/consent/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package guru.core.consent + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("guru.core.consent.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/src/main/AndroidManifest.xml b/GuruConsent-Android/GuruConsent/src/main/AndroidManifest.xml new file mode 100644 index 0000000..942f10d --- /dev/null +++ b/GuruConsent-Android/GuruConsent/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/AdsAnnotation.kt b/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/AdsAnnotation.kt new file mode 100644 index 0000000..8ab50db --- /dev/null +++ b/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/AdsAnnotation.kt @@ -0,0 +1,85 @@ +package guru.core.consent.gdpr + +import androidx.annotation.IntDef +import com.google.android.ump.ConsentDebugSettings +import com.google.android.ump.ConsentInformation +import com.google.android.ump.FormError + +@IntDef( + AdsRes.LAYOUT, + AdsRes.HEADLINE, + AdsRes.BODY, + AdsRes.CALL_TO_ACTION, + AdsRes.APP_ICON, + AdsRes.ADVERTISER, + AdsRes.MEDIA, + AdsRes.AD_CHOICES, + AdsRes.PRICE, + AdsRes.STORE, + AdsRes.STAR_RATING +) +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.FIELD, + AnnotationTarget.LOCAL_VARIABLE, + AnnotationTarget.CLASS +) +annotation class AdsRes { + companion object { + const val LAYOUT = 0 + const val HEADLINE = 1 + const val BODY = 2 + const val CALL_TO_ACTION = 3 + const val APP_ICON = 4 + const val ADVERTISER = 5 + const val MEDIA = 6 + const val AD_CHOICES = 7 + const val PRICE = 8 + const val STORE = 9 + const val STAR_RATING = 10 + } +} + +@IntDef(ConsentDebugGeography.DISABLED, ConsentDebugGeography.EEA, ConsentDebugGeography.NOT_EEA) +@Retention(AnnotationRetention.SOURCE) +annotation class ConsentDebugGeography { + companion object { + const val DISABLED = ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_DISABLED + const val EEA = ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA + const val NOT_EEA = ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA + } +} + +@IntDef( + ConsentStatus.NOT_AVAILABLE, ConsentStatus.UNKNOWN, + ConsentStatus.NOT_REQUIRED, ConsentStatus.REQUIRED, ConsentStatus.OBTAINED +) +@Retention(AnnotationRetention.SOURCE) +annotation class ConsentStatus { + companion object { + // Consent Form Not Available + const val NOT_AVAILABLE = -100 + + // Unknown consent status. + const val UNKNOWN = ConsentInformation.ConsentStatus.UNKNOWN + + // User consent required but not yet obtained. + const val NOT_REQUIRED = ConsentInformation.ConsentStatus.NOT_REQUIRED + + // User consent not required. For example, the user is not in the EEA or the UK. + const val REQUIRED = ConsentInformation.ConsentStatus.REQUIRED + + // User consent obtained. Personalization not defined. + const val OBTAINED = ConsentInformation.ConsentStatus.OBTAINED + + fun toName(@ConsentStatus status: Int) = when (status) { + UNKNOWN -> "UNKNOWN" + NOT_REQUIRED -> "NOT_REQUIRED" + OBTAINED -> "OBTAINED" + REQUIRED -> "REQUIRED" + else -> "NOT_AVAILABLE" + } + } +} \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/ConsentManager.kt b/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/ConsentManager.kt new file mode 100644 index 0000000..268abff --- /dev/null +++ b/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/ConsentManager.kt @@ -0,0 +1,58 @@ +package guru.core.consent.gdpr + +import android.app.Activity +import androidx.annotation.Keep +import com.google.android.ump.ConsentForm +import com.google.android.ump.ConsentInformation +import com.google.android.ump.UserMessagingPlatform + +@Keep +class ConsentManager(private val activity: Activity) { + private val consentInformation: ConsentInformation = + UserMessagingPlatform.getConsentInformation(activity) + + /** Interface definition for a callback to be invoked when consent gathering is complete. */ + + + /** Helper variable to determine if the app can request ads. */ + val canRequestAds: Boolean + get() = consentInformation.canRequestAds() + + /** Helper variable to determine if the privacy options form is required. */ + val isPrivacyOptionsRequired: Boolean + get() = + consentInformation.privacyOptionsRequirementStatus == + ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED + + /** + * Helper method to call the UMP SDK methods to request consent information and load/show a + * consent form if necessary. + */ + fun gather(request: ConsentRequest) { + + // Requesting an update to consent information should be called on every app launch. + consentInformation.requestConsentInfoUpdate( + activity, + request.params, + { + UserMessagingPlatform.loadAndShowConsentFormIfRequired(activity) { formError -> + // Consent has been gathered. + request.consentGatheringCompleteListener?.consentGatheringComplete(formError) + } + }, + { requestConsentError -> + request.consentGatheringCompleteListener?.consentGatheringComplete( + requestConsentError + ) + } + ) + } + + /** Helper method to call the UMP SDK method to show the privacy options form. */ + fun showPrivacyOptionsForm( + activity: Activity, + onConsentFormDismissedListener: ConsentForm.OnConsentFormDismissedListener + ) { + UserMessagingPlatform.showPrivacyOptionsForm(activity, onConsentFormDismissedListener) + } +} \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/ConsentRequest.kt b/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/ConsentRequest.kt new file mode 100644 index 0000000..d1e6b8c --- /dev/null +++ b/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/ConsentRequest.kt @@ -0,0 +1,83 @@ +package guru.core.consent.gdpr + +import android.app.Activity +import androidx.annotation.Keep +import com.google.android.ump.ConsentDebugSettings +import com.google.android.ump.ConsentRequestParameters +import com.google.android.ump.FormError + +@Keep +class ConsentRequest( + val activity: Activity, + val params: ConsentRequestParameters, + val listener: Listener?, + val consentGatheringCompleteListener: OnConsentGatheringCompleteListener? +) { + + interface Listener { + fun onConsentResult(@ConsentStatus status: Int) + + fun onConsentImpression() + + fun onConsentLoadFailure() + } + + fun interface OnConsentGatheringCompleteListener { + fun consentGatheringComplete(error: FormError?) + } + + @Keep + data class Builder(val activity: Activity) { + private var tagForUnderAgeOfConsent: Boolean = false + private var testDeviceIds: HashSet = HashSet() + private var consentListener: Listener? = null + private var consentGatheringCompleteListener: OnConsentGatheringCompleteListener? = null + + @ConsentDebugGeography + var debugGeography: Int? = null + + fun debugGeography(@ConsentDebugGeography geography: Int?) = apply { + debugGeography = geography + } + + fun tagForUnderAgeOfConsent(enabled: Boolean) = apply { + tagForUnderAgeOfConsent = enabled + } + + fun addDeviceIds(ids: Set) = apply { + testDeviceIds.addAll(ids) + } + + fun withListener(listener: Listener) = apply { + consentListener = listener + } + + fun withConsentGatheringCompleteListener(listener: OnConsentGatheringCompleteListener) = + apply { + consentGatheringCompleteListener = listener + } + + fun build(): ConsentRequest { + val builder = ConsentRequestParameters.Builder() + if (debugGeography != null || testDeviceIds.isNotEmpty()) { + builder.setConsentDebugSettings( + ConsentDebugSettings.Builder(activity).let { + debugGeography?.apply { + it.setDebugGeography(this) + } + for (id in testDeviceIds) { + it.addTestDeviceHashedId(id) + } + it.build() + }) + + } + return ConsentRequest( + activity, + builder.build(), + consentListener, + consentGatheringCompleteListener + ) + } + } +} \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/GdprHelper.kt b/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/GdprHelper.kt new file mode 100644 index 0000000..c1ccc39 --- /dev/null +++ b/GuruConsent-Android/GuruConsent/src/main/java/guru/core/consent/gdpr/GdprHelper.kt @@ -0,0 +1,101 @@ +package guru.core.consent.gdpr + +import android.app.Activity +import android.content.Context +import androidx.annotation.Keep +import com.google.android.ump.ConsentForm +import com.google.android.ump.ConsentInformation +import com.google.android.ump.UserMessagingPlatform + +@Deprecated("Use ConsentManager instead") +@Keep +object GdprHelper { + + var consentForm: ConsentForm? = null + + fun request(request: ConsentRequest) { + val consentInformation = UserMessagingPlatform.getConsentInformation(request.activity) + consentInformation.requestConsentInfoUpdate( + request.activity, + request.params, + { + // The consent information state was updated. + // You are now ready to check if a form is available. + if (consentInformation.isConsentFormAvailable) { + loadForm(request) + } else { + request.listener?.onConsentResult(ConsentStatus.NOT_AVAILABLE) + } + }, + { + // Handle the error. +// Timber.w("GdprHelper request error! ${it.message}") + request.listener?.onConsentLoadFailure() + }) + } + + private fun loadForm(request: ConsentRequest) { + UserMessagingPlatform.loadConsentForm( + request.activity, + { consentForm -> + run { + GdprHelper.consentForm = consentForm + val consentInformation = UserMessagingPlatform.getConsentInformation(request.activity) + if (consentInformation.consentStatus == ConsentInformation.ConsentStatus.REQUIRED) { + request.listener?.onConsentImpression() + consentForm.show(request.activity) { // Handle dismissal by reloading form. + checkForm(request) + } + } else { + request.listener?.onConsentResult(consentInformation.consentStatus) + grantConsent(request.activity) + } + } + }, + { + // Handle the error +// Timber.w("GdprHelper loadForm error! ${it.message}") + request.listener?.onConsentLoadFailure() + } + ) + } + + private fun checkForm(request: ConsentRequest) { + UserMessagingPlatform.loadConsentForm( + request.activity, + { consentForm -> + run { + GdprHelper.consentForm = consentForm + val consentInformation = UserMessagingPlatform.getConsentInformation(request.activity) + request.listener?.onConsentResult(consentInformation.consentStatus) + grantConsent(request.activity) + } + }, + { + // Handle the error +// Timber.w("GdprHelper loadForm error! ${it.message}") + request.listener?.onConsentLoadFailure() + } + ) + } + + + private fun grantConsent(context: Context) { + // Under the Google EU User Consent Policy, you must ensure that certain disclosures are given to, + // and consents obtained from, users in the European Economic Area (EEA) regarding the use of device + // identifiers and personal data. This policy reflects the requirements of the EU ePrivacy Directive + // and the General Data Protection Regulation (GDPR). When seeking consent, you must identify each ad + // network in your mediation chain that may collect, receive, or use personal data and provide information + // about each network's use. Google currently is unable to pass the user's consent choice to such + // networks automatically. + } + + private fun revokeConsent() { + + } + + fun reset(activity: Activity) { + val consentInformation = UserMessagingPlatform.getConsentInformation(activity) + consentInformation.reset() + } +} \ No newline at end of file diff --git a/GuruConsent-Android/GuruConsent/src/test/java/guru/core/consent/ExampleUnitTest.kt b/GuruConsent-Android/GuruConsent/src/test/java/guru/core/consent/ExampleUnitTest.kt new file mode 100644 index 0000000..b71ae6a --- /dev/null +++ b/GuruConsent-Android/GuruConsent/src/test/java/guru/core/consent/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package guru.core.consent + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/GuruConsent-Android/app/.gitignore b/GuruConsent-Android/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/GuruConsent-Android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/GuruConsent-Android/app/build.gradle b/GuruConsent-Android/app/build.gradle new file mode 100644 index 0000000..cf1194b --- /dev/null +++ b/GuruConsent-Android/app/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "com.example.gurugdprexample" + minSdk 21 + targetSdk 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.5.10' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.google.android.material:material:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + +// implementation 'guru.core.consent:GuruConsent:1.0.0' +} \ No newline at end of file diff --git a/GuruConsent-Android/app/proguard-rules.pro b/GuruConsent-Android/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/GuruConsent-Android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/GuruConsent-Android/app/src/androidTest/java/com/example/gurugdprexample/ExampleInstrumentedTest.kt b/GuruConsent-Android/app/src/androidTest/java/com/example/gurugdprexample/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..35ba216 --- /dev/null +++ b/GuruConsent-Android/app/src/androidTest/java/com/example/gurugdprexample/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.gurugdprexample + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.gurugdprexample", appContext.packageName) + } +} \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/AndroidManifest.xml b/GuruConsent-Android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..01069d8 --- /dev/null +++ b/GuruConsent-Android/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/java/com/example/gurugdprexample/MainActivity.kt b/GuruConsent-Android/app/src/main/java/com/example/gurugdprexample/MainActivity.kt new file mode 100644 index 0000000..ee1256b --- /dev/null +++ b/GuruConsent-Android/app/src/main/java/com/example/gurugdprexample/MainActivity.kt @@ -0,0 +1,11 @@ +package com.example.gurugdprexample + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/GuruConsent-Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/drawable/ic_launcher_background.xml b/GuruConsent-Android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GuruConsent-Android/app/src/main/res/layout/activity_main.xml b/GuruConsent-Android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..17eab17 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/GuruConsent-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/GuruConsent-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/GuruConsent-Android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/GuruConsent-Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/GuruConsent-Android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/GuruConsent-Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/GuruConsent-Android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/GuruConsent-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/GuruConsent-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/GuruConsent-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/GuruConsent-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/GuruConsent-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/GuruConsent-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/GuruConsent-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/GuruConsent-Android/app/src/main/res/values-night/themes.xml b/GuruConsent-Android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..c6bc617 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/values/colors.xml b/GuruConsent-Android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/values/strings.xml b/GuruConsent-Android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..b0fa8b7 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + GuruGdprExample + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/values/themes.xml b/GuruConsent-Android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..819ced5 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/xml/backup_rules.xml b/GuruConsent-Android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/main/res/xml/data_extraction_rules.xml b/GuruConsent-Android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/GuruConsent-Android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/GuruConsent-Android/app/src/test/java/com/example/gurugdprexample/ExampleUnitTest.kt b/GuruConsent-Android/app/src/test/java/com/example/gurugdprexample/ExampleUnitTest.kt new file mode 100644 index 0000000..46f0590 --- /dev/null +++ b/GuruConsent-Android/app/src/test/java/com/example/gurugdprexample/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.gurugdprexample + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/GuruConsent-Android/build.gradle b/GuruConsent-Android/build.gradle new file mode 100644 index 0000000..7b9b67d --- /dev/null +++ b/GuruConsent-Android/build.gradle @@ -0,0 +1,10 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.2.1' apply false + id 'com.android.library' version '7.2.1' apply false + id 'org.jetbrains.kotlin.android' version '1.6.10' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/GuruConsent-Android/gradle.properties b/GuruConsent-Android/gradle.properties new file mode 100644 index 0000000..3c7a8bd --- /dev/null +++ b/GuruConsent-Android/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/GuruConsent-Android/gradle/wrapper/gradle-wrapper.jar b/GuruConsent-Android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/GuruConsent-Android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/GuruConsent-Android/gradle/wrapper/gradle-wrapper.properties b/GuruConsent-Android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9c4bdc4 --- /dev/null +++ b/GuruConsent-Android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Nov 11 12:05:16 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/GuruConsent-Android/gradlew b/GuruConsent-Android/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/GuruConsent-Android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/GuruConsent-Android/gradlew.bat b/GuruConsent-Android/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/GuruConsent-Android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/GuruConsent-Android/settings.gradle b/GuruConsent-Android/settings.gradle new file mode 100644 index 0000000..819381d --- /dev/null +++ b/GuruConsent-Android/settings.gradle @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + mavenLocal() + + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + mavenLocal() + } +} +rootProject.name = "GuruGdprExample" +include ':app' +include ':GuruConsent' diff --git a/guru_analytics/.gitignore b/guru_analytics/.gitignore new file mode 100644 index 0000000..bb2a2bd --- /dev/null +++ b/guru_analytics/.gitignore @@ -0,0 +1,14 @@ +.gradle +/captures +/local.properties +/.idea/workspace.xml +.DS_Store +/build +.idea/ +*iml +*.iml +*/build +/lib +wh.properties +/atom +*.txt \ No newline at end of file diff --git a/guru_analytics/CHANGELOG.md b/guru_analytics/CHANGELOG.md new file mode 100644 index 0000000..d653056 --- /dev/null +++ b/guru_analytics/CHANGELOG.md @@ -0,0 +1,138 @@ +## v0.3.1 + +##### Feature +- modify FG event code logic + +## v0.3.0 + +##### Feature +- modify event handle and add dns error event +- init remove isCallbackEventHandler / add setEventHandlerCallback +- EventHandler add ERROR_ZIP(107) callback +- EventHandler ERROR_API(101)/ERROR_CACHE_CONTROL(103) add error message +- model Event/ParamValue add @Keep + +## v0.2.11 + +##### Feature +- init add setMainProcess(isNotBlack check main process) +- add error_process logevent + +## v0.2.10 + +##### Feature +- fg event logic adjustment +- isInitPeriodicWork default false +- add remove user properties function + +## v0.2.9 + +##### BugFix +- GuruAnalyticsImpl delivers replace with CopyOnWriteArrayList + +##### Feature +- add app start first fg event + +## v0.2.8 + +##### Feature + +- 删除androidx.appcompat:appcompat库 + +## v0.2.7 + +##### Feature + +- 修复fg埋点偶现未能清空本地累计时间的问题 + +## v0.2.6 + +##### Feature + +- Header中增加X-APP-ID / X-DEVICE-INFO, 初始化时增加对应设置方法 + event结构体中新增eventId, 采用uuid4算法(小写) + +## v0.2.5 + +##### Feature + +- 前台时长打点(fg)获取方法调整, 修改为每间隔30秒(时长可调)生成一个fg打点 + 初始化增加setFgEventPeriodInSeconds()方法,设置fg埋点间隔上报时间,默认30秒 + +## v0.2.4 + +##### Feature + +- 支持配置上报打点api的baseUrl. + 通过builder初始化时增加方法setUploadEventBaseUrl(), 初始化完成后也可以通过调用setUploadEventBaseUrl()实现功能 + +## v0.2.3 + +##### Feature + +- 优化统计时长机制,使用System.currentTimeMillis()替换成SystemClock.elapsedRealtime() +- 添加各个节点hook机制 +- 针对unity项目添加worker规避机制 + +## v0.2.2 + +##### Feature + +- 优化上传机制,添加打开后强制上传机制 + +##### BugFix + +- 修复SCREEN_H上报错误问题 +## v0.2.1 + +- 修复Timber.DebugTree的问题 + +## v0.2.0 + +##### Feature + +- 在初始化延时期间,所有产生的事件都将等待初始化完成后才会真正分发 +- 添加本地日志收集机制,方便追溯问题 + +##### BugFix + +- 修复后台上报时缺失设备信息的问题 +## v0.1.1 + +##### BugFix + +- 修复Worker后台上报时崩溃问题 + +## v0.1.0 + +- 间隔`uploadPeriod`秒后打包上传`batchLimit`个数据 + +- 添加Worker处理未发送事件(`6小时`) + +- 支持延时启动上传的逻辑,初始化后延时`startUploadDelay`秒后启动上传逻辑 + +- 支持事件优先级 + + | ***NAME*** | ***PRIORITY*** | + | :-------- | :--------: | + | `EMERGENCE` | 0 | + | `HIGH` | 5 | + | `DEFAULT` | 10 | + | `LOW` | 15 | + +- 首次初始化时自动上报`FirstOpen`点,并将该点以`EMERGENCE`优先级发送 + +- 支持App生命周期自打点`fg`,并根据前后台时间计算`duration` + +- 提供下列接口 + + | ***Method*** | ***Description*** | + | :-------- | :--------: | + | `logEvent` | 所有event点都是通过该函数完成 | + | `setUserProperty` | 设置用户属性 | + | `setScreen` | 设置当前屏幕 | + | `setDeviceId` | 设置设备ID | + | `setUid` | 设置用户ID | + | `setAdjustId` | 设置AdjustId | + | `setAdId` | 设置Google Ad Id | + | `setFirebaseId` | 设置Firebase Id | diff --git a/guru_analytics/app/.gitignore b/guru_analytics/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/guru_analytics/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/guru_analytics/app/build.gradle b/guru_analytics/app/build.gradle new file mode 100644 index 0000000..73d9067 --- /dev/null +++ b/guru_analytics/app/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "com.example.guruanalytics" + minSdk 21 + targetSdk 32 + versionCode 1 + versionName "1.1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.5.0' + implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + implementation(project(':guru_analytics')) +// implementation 'guru.core.analytics:guru_analytics:0.1.0' +} \ No newline at end of file diff --git a/guru_analytics/app/proguard-rules.pro b/guru_analytics/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/guru_analytics/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/guru_analytics/app/src/androidTest/java/com/example/guruanalytics/ExampleInstrumentedTest.kt b/guru_analytics/app/src/androidTest/java/com/example/guruanalytics/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..1c413a5 --- /dev/null +++ b/guru_analytics/app/src/androidTest/java/com/example/guruanalytics/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.guruanalytics + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.guruanalytics", appContext.packageName) + } +} \ No newline at end of file diff --git a/guru_analytics/app/src/main/AndroidManifest.xml b/guru_analytics/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d1ad612 --- /dev/null +++ b/guru_analytics/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/java/com/example/guruanalytics/MainActivity.kt b/guru_analytics/app/src/main/java/com/example/guruanalytics/MainActivity.kt new file mode 100644 index 0000000..fa026db --- /dev/null +++ b/guru_analytics/app/src/main/java/com/example/guruanalytics/MainActivity.kt @@ -0,0 +1,92 @@ +package com.example.guruanalytics + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import android.widget.TextView +import guru.core.analytics.GuruAnalytics +import guru.core.analytics.data.db.model.EventPriority +import guru.core.analytics.data.model.AnalyticsOptions +import guru.core.analytics.handler.AnalyticsCode + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + +// GuruAnalytics.INSTANCE.initialize( +// this, +// batchLimit = 25, +// startUploadDelayInSecond = 3L, +// debug = true, +// callbackEventHandler = true, +// ) + + GuruAnalytics.Builder(this) + .setBatchLimit(25) + .setUploadPeriodInSeconds(60) + .setStartUploadDelayInSecond(3) + .setEventExpiredInDays(7) + .isPersistableLog(true) + .setEventHandlerCallback(eventHandler) + .isInitPeriodicWork(false) + .isDebug(BuildConfig.DEBUG) +// .setUploadEventBaseUrl("https://www.baidu.com") +// .setFgEventPeriodInSeconds(60L) + .setXAppId("test_x_app_id") + .setXDeviceInfo("test_x_device_info") + .setMainProcess("com.example.guruanalytics") + .build() + + findViewById(R.id.tvLogEvent).setOnClickListener { + val map = mutableMapOf() + map["percent"] = 0.4 + map["level"] = 2 + map["from"] = "game" + GuruAnalytics.INSTANCE.logEvent("test_event", "game", "main", 10, map) + } + findViewById(R.id.tvOpenTestProcessActivity).setOnClickListener { + TestProcessActivity.startActivity(this) + } + findViewById(R.id.tvLocalLog).setOnClickListener { + val startTime = System.currentTimeMillis() + GuruAnalytics.INSTANCE.zipLogs(this) + Log.i("get_local_log", "${System.currentTimeMillis() - startTime}") + } + findViewById(R.id.tvEventStatistic).setOnClickListener { + GuruAnalytics.INSTANCE.getEventsStatics().run { + Log.i( + "UploadEventDaemon_main", + "$eventCountAll $eventCountDeleted $eventCountUploaded" + ) + } + } + findViewById(R.id.tvEventEmergence).setOnClickListener { + val map = mutableMapOf() + map["percent"] = 0.4 + map["level"] = 2 + map["from"] = "game" + GuruAnalytics.INSTANCE.logEvent( + "test_event_emergence", "game", "main", 10, map, options = AnalyticsOptions( + EventPriority.EMERGENCE + ) + ) + } + findViewById(R.id.tvBaseUrl).setOnClickListener { + GuruAnalytics.INSTANCE.setUploadEventBaseUrl(this, "https://www.castbox.fm/") + } + + GuruAnalytics.INSTANCE.setScreen("main") + GuruAnalytics.INSTANCE.setAdId("AD_ID_01") + GuruAnalytics.INSTANCE.setUserProperty("uid", "110051") + } + + private val eventHandler: (Int, String?) -> Unit = { code, ext -> + Log.i("eventHandler", "$code${if (ext.isNullOrBlank()) "" else " $ext"}") + } + + override fun onDestroy() { + GuruAnalytics.INSTANCE.removeEventHandler(eventHandler) + super.onDestroy() + } +} \ No newline at end of file diff --git a/guru_analytics/app/src/main/java/com/example/guruanalytics/TestProcessActivity.kt b/guru_analytics/app/src/main/java/com/example/guruanalytics/TestProcessActivity.kt new file mode 100644 index 0000000..05673c1 --- /dev/null +++ b/guru_analytics/app/src/main/java/com/example/guruanalytics/TestProcessActivity.kt @@ -0,0 +1,50 @@ +package com.example.guruanalytics + +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import android.widget.TextView +import guru.core.analytics.GuruAnalytics + +class TestProcessActivity : AppCompatActivity() { + + companion object { + fun startActivity(context: Context) { + val intent = Intent(context, TestProcessActivity::class.java) + context.startActivity(intent) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_test_process) + + GuruAnalytics.Builder(this) + .setBatchLimit(25) + .setUploadPeriodInSeconds(60) + .setStartUploadDelayInSecond(3) + .setEventExpiredInDays(7) + .isPersistableLog(true) + .isInitPeriodicWork(true) + .isDebug(BuildConfig.DEBUG) + .setXAppId("test_x_app_id") + .setXDeviceInfo("test_x_device_info") + .setMainProcess("com.example.guruanalytics") + .build() + + findViewById(R.id.tvFinish).setOnClickListener { + finish() + } + findViewById(R.id.tvLogEventProcess).setOnClickListener { + val map = mutableMapOf() + map["percent"] = 0.4 + map["level"] = 2 + map["from"] = "game" + GuruAnalytics.INSTANCE.logEvent("test_event", "game", "main", 10, map) + Log.e("initialize","tvLogEventProcess") + } + } + +} \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/guru_analytics/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/guru_analytics/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/drawable/ic_launcher_background.xml b/guru_analytics/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/guru_analytics/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_analytics/app/src/main/res/layout/activity_main.xml b/guru_analytics/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..82ccbf3 --- /dev/null +++ b/guru_analytics/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/layout/activity_test_process.xml b/guru_analytics/app/src/main/res/layout/activity_test_process.xml new file mode 100644 index 0000000..d935d0e --- /dev/null +++ b/guru_analytics/app/src/main/res/layout/activity_test_process.xml @@ -0,0 +1,44 @@ + + + + + + + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/guru_analytics/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/guru_analytics/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/guru_analytics/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/guru_analytics/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/guru_analytics/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/guru_analytics/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/guru_analytics/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/guru_analytics/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/guru_analytics/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/guru_analytics/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/guru_analytics/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/guru_analytics/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/guru_analytics/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/guru_analytics/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/guru_analytics/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/guru_analytics/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/guru_analytics/app/src/main/res/values-night/themes.xml b/guru_analytics/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..0b66ac2 --- /dev/null +++ b/guru_analytics/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/values/colors.xml b/guru_analytics/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/guru_analytics/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/values/strings.xml b/guru_analytics/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..cad548b --- /dev/null +++ b/guru_analytics/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + GuruAnalytics + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/values/themes.xml b/guru_analytics/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..9c53c38 --- /dev/null +++ b/guru_analytics/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/xml/backup_rules.xml b/guru_analytics/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/guru_analytics/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/main/res/xml/data_extraction_rules.xml b/guru_analytics/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/guru_analytics/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/guru_analytics/app/src/test/java/com/example/guruanalytics/ExampleUnitTest.kt b/guru_analytics/app/src/test/java/com/example/guruanalytics/ExampleUnitTest.kt new file mode 100644 index 0000000..9eeea4d --- /dev/null +++ b/guru_analytics/app/src/test/java/com/example/guruanalytics/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.guruanalytics + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/guru_analytics/build.gradle b/guru_analytics/build.gradle new file mode 100644 index 0000000..0a9f125 --- /dev/null +++ b/guru_analytics/build.gradle @@ -0,0 +1,25 @@ +buildscript { + ext.kotlin_version = '1.6.10' + + repositories { +// maven { url 'http://localhost:8081/repository/maven-public/' } + google() + mavenCentral() + mavenLocal() + + } + + dependencies { + classpath "com.android.tools.build:gradle:4.2.1" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { +// maven { url 'http://localhost:8081/repository/maven-public/' } + mavenCentral() + google() + mavenLocal() + } +} \ No newline at end of file diff --git a/guru_analytics/gradle.properties b/guru_analytics/gradle.properties new file mode 100644 index 0000000..cd0519b --- /dev/null +++ b/guru_analytics/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/guru_analytics/gradle/wrapper/gradle-wrapper.jar b/guru_analytics/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/guru_analytics/gradle/wrapper/gradle-wrapper.jar differ diff --git a/guru_analytics/gradle/wrapper/gradle-wrapper.properties b/guru_analytics/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ffed3a2 --- /dev/null +++ b/guru_analytics/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/guru_analytics/gradlew b/guru_analytics/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/guru_analytics/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/guru_analytics/gradlew.bat b/guru_analytics/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/guru_analytics/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/guru_analytics/guru_analytics/.gitignore b/guru_analytics/guru_analytics/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/guru_analytics/guru_analytics/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/guru_analytics/guru_analytics/build.gradle b/guru_analytics/guru_analytics/build.gradle new file mode 100644 index 0000000..6c89681 --- /dev/null +++ b/guru_analytics/guru_analytics/build.gradle @@ -0,0 +1,59 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'maven-publish' +} + +apply from: 'dependencies.gradle' +apply from: 'maven-publish.gradle' + +android { + compileSdk android.compileSdk + + defaultConfig { +// applicationId "guru.core.analytics" + minSdk android.minSdk + targetSdk android.targetSdk + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + api fileTree(dir: 'libs', include: ['*.jar']) + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + implementation basicDependencies + + kapt kaptDependencies + + implementation roomDependencies + + implementation retrofitDependencies + + implementation okhttpDependencies + + implementation process + + implementation workerDependencies +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/dependencies.gradle b/guru_analytics/guru_analytics/dependencies.gradle new file mode 100644 index 0000000..35cffd3 --- /dev/null +++ b/guru_analytics/guru_analytics/dependencies.gradle @@ -0,0 +1,60 @@ +ext { + compiler = [ + java : JavaVersion.VERSION_1_8, + kotlin: '1.6.10' + ] + + android = [ + buildTools: '30.0.3', + minSdk : 21, + targetSdk : 32, + compileSdk: 32 + ] + + androidXCoreVersion = '1.7.0' + timberVersion = '4.7.1' + roomVersion = '2.4.3' + gsonVersion = '2.8.5' + + retrofitVersion = '2.7.1' + okhttpVersion = '4.9.3' + preferenceVersion = '1.2.0' + processVersion = '2.4.0' + workVersion = '2.7.1' + + kaptDependencies = [ + "androidx.room:room-compiler:$roomVersion", + ] + + basicDependencies = [ + "androidx.core:core:$androidXCoreVersion", + "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${compiler.kotlin}", + "com.jakewharton.timber:timber:$timberVersion", + "com.google.code.gson:gson:$gsonVersion", + ] + + roomDependencies = [ + "androidx.room:room-runtime:$roomVersion", + "androidx.room:room-rxjava2:$roomVersion" + ] + + retrofitDependencies = [ + "com.squareup.retrofit2:retrofit:$retrofitVersion", + "com.squareup.retrofit2:converter-gson:$retrofitVersion", + "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion" + ] + + okhttpDependencies = [ + "com.squareup.okhttp3:okhttp:$okhttpVersion" + ] + + workerDependencies = [ + "androidx.work:work-runtime:$workVersion", + "androidx.work:work-runtime-ktx:$workVersion", + "androidx.work:work-rxjava2:$workVersion" + ] + + process = [ + "androidx.lifecycle:lifecycle-process:$processVersion" + ] +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/maven-publish.gradle b/guru_analytics/guru_analytics/maven-publish.gradle new file mode 100644 index 0000000..a306d6d --- /dev/null +++ b/guru_analytics/guru_analytics/maven-publish.gradle @@ -0,0 +1,76 @@ +publishing { // Repositories *to* which Gradle can publish artifacts + repositories { RepositoryHandler handler -> + handler.mavenLocal() + maven { + url "$buildDir/repo" // change to point to your repo, e.g. http://my.org/repo + } +// maven { +// url "http://localhost:8081/repository/maven-releases/" +// credentials { +// username "admin" +// password "" +// } +// } + } + publications { PublicationContainer publication -> + + // Creates a Maven publication called "myPublication". + maven(MavenPublication) { + groupId 'guru.core.analytics' + artifactId 'guru_analytics' + version '0.3.1' // Your package version +// artifact publishArtifact //Example: *./target/myJavaClasses.jar* +// artifact "build/outputs/aar/aar-test-release.aar"//aar包的目录 + afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) } + + //带上依赖 ,否则会报错 + pom.withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + def scopes = [] + if (configurations.hasProperty("api")) { + scopes.add(configurations.api) + } + if (configurations.hasProperty("implementation")) { + scopes.add(configurations.implementation) + } + if (configurations.hasProperty("debugImplementation")) { + scopes.add(configurations.debugImplementation) + } + if (configurations.hasProperty("releaseImplementation")) { + scopes.add(configurations.releaseImplementation) + } + +// if (project.ext.targetType != "jar") { +// scopes.add(configurations.provided) +// } + + scopes.each { scope -> + scope.allDependencies.each { + if (it instanceof ModuleDependency) { + boolean isTransitive = ((ModuleDependency) it).transitive + if (!isTransitive) { + println "<<<< not transitive dependency: [${it.group}, ${it.name}, ${it.version}]" + return + } + } + + if (it.group == "${project.rootProject.name}.libs" || it.version == 'unspecified') { + return + } + + if (it.group && it.name && it.version) { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + dependencyNode.appendNode('scope', scope.name) + } + } + } + } + } + } + + +} diff --git a/guru_analytics/guru_analytics/proguard-rules.pro b/guru_analytics/guru_analytics/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/guru_analytics/guru_analytics/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/androidTest/java/guru/data/ExampleInstrumentedTest.kt b/guru_analytics/guru_analytics/src/androidTest/java/guru/data/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..e9d60be --- /dev/null +++ b/guru_analytics/guru_analytics/src/androidTest/java/guru/data/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package guru.data + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("guru.data", appContext.packageName) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/AndroidManifest.xml b/guru_analytics/guru_analytics/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c0f6a45 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/Constants.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/Constants.kt new file mode 100644 index 0000000..f9cfbb1 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/Constants.kt @@ -0,0 +1,71 @@ +package guru.core.analytics + + +object Constants { + + const val VERSION = 10 + + object Event { + const val VERSION = "version" + const val TIMESTAMP = "timestamp" + + const val EVENT = "event" + const val PARAM = "param" + const val SCREEN = "screen_name" + const val ITEM_CATEGORY = "item_category" + const val ITEM_NAME = "item_name" + const val VALUE = "value" + const val FG = "fg" + const val DURATION = "duration" + const val FIRST_OPEN = "first_open" + const val ERROR_PROCESS = "error_process" + const val PROCESS = "process" + } + + object Ids { + const val DEVICE_ID = "deviceId" + const val UID = "uid" + const val ADJUST_ID = "adjustId" + const val AD_ID = "adId" + const val FIREBASE_ID = "firebaseId" + } + + object DeviceInfo { + const val APP_ID = "appId" + const val PLATFORM = "platform" + const val COUNTRY = "country" + const val VERSION = "version" + const val TZ_OFFSET = "tzOffset" + const val DEVICE_TYPE = "deviceType" + const val BRAND = "brand" + const val MODEL = "model" + const val SCREEN_H = "screenH" + const val SCREEN_W = "screenW" + const val OS_VERSION = "osVersion" + const val LANGUAGE = "language" + } + + object Properties { + const val FIRST_OPEN_TIME = "first_open_time" + const val B_LEVEL = "b_level" + const val B_PLAY = "b_play" + const val GRADE = "grade" + const val IS_IAP_USER = "is_iap_user" + const val IAP_COIN = "iap_coin" + const val NONIAP_COIN = "noniap_coin" + const val COIN = "coin" + const val EXP = "exp" + const val HP = "hp" + } + + object DeviceType { + const val TABLET = "tablet" + const val MOBILE = "mobile" + const val PC = "pc" + } + + object Platform { + const val ANDROID = "ANDROID" + const val IOS = "IOS" + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/GuruAnalytics.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/GuruAnalytics.kt new file mode 100644 index 0000000..874092f --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/GuruAnalytics.kt @@ -0,0 +1,141 @@ +package guru.core.analytics + +import android.content.Context +import guru.core.analytics.data.db.model.EventStatistic +import guru.core.analytics.data.model.AnalyticsInfo +import guru.core.analytics.data.model.AnalyticsOptions +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.impl.GuruAnalyticsImpl +import java.io.File + +/** + * Created by Haoyi on 2022-11-05. + */ +abstract class GuruAnalytics { + + protected abstract fun initialize( + context: Context, + batchLimit: Int? = null, + uploadPeriodInSeconds: Long? = null, + startUploadDelayInSecond: Long? = 0L, + eventExpiredInDays: Int? = 7, + debug: Boolean = false, + persistableLog: Boolean = true, + listener: ((Int, String?) -> Unit)? = null, + isInitPeriodicWork: Boolean = false, + uploadEventBaseUrl: String? = null, + fgEventPeriodInSeconds: Long? = null, + xAppId: String? = null, + xDeviceInfo: String? = null, + mainProcess: String? = null, + ) + + abstract fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String) + + abstract fun isDebug(): Boolean + + abstract fun setDebug(debug: Boolean) + + abstract fun setDeviceId(deviceId: String) + + abstract fun setUid(uid: String) + + abstract fun setAdjustId(adjustId: String) + + abstract fun setAdId(adId: String) + + abstract fun setFirebaseId(firebaseId: String) + + abstract fun setScreen(screenName: String) + + abstract fun zipLogs(context: Context): File? + + abstract fun logEvent( + eventName: String, + itemCategory: String? = null, + itemName: String? = null, + value: Number? = null, + parameters: Map? = null, + options: AnalyticsOptions = AnalyticsOptions() + ) + + abstract fun setUserProperty(key: String, value: String) + + abstract fun getEventsStatics(): EventStatistic + + abstract fun addEventHandler(listener: ((Int, String?) -> Unit)) + + abstract fun removeEventHandler(listener: ((Int, String?) -> Unit)) + + abstract fun removeUserProperty(key: String) + + abstract fun removeUserProperties(keys: Set) + + companion object { + val INSTANCE: GuruAnalytics by lazy() { + GuruAnalyticsImpl() + } + } + + class Builder(val context: Context) { + private val analyticsInfo = AnalyticsInfo() + + fun setBatchLimit(batchLimit: Int?) = apply { analyticsInfo.batchLimit = batchLimit } + + fun setUploadPeriodInSeconds(uploadPeriodInSeconds: Long?) = + apply { analyticsInfo.uploadPeriodInSeconds = uploadPeriodInSeconds } + + fun setStartUploadDelayInSecond(startUploadDelayInSecond: Long?) = + apply { analyticsInfo.startUploadDelayInSecond = startUploadDelayInSecond } + + fun setEventExpiredInDays(eventExpiredInDays: Int?) = + apply { analyticsInfo.eventExpiredInDays = eventExpiredInDays } + + fun isPersistableLog(persistableLog: Boolean) = + apply { analyticsInfo.persistableLog = persistableLog } + + fun setEventHandlerCallback(eventHandlerCallback: ((Int, String?) -> Unit)) = + apply { analyticsInfo.eventHandlerCallback = eventHandlerCallback } + + fun isDebug(debug: Boolean) = apply { analyticsInfo.debug = debug } + + fun isInitPeriodicWork(isInit: Boolean) = apply { analyticsInfo.isInitPeriodicWork = isInit } + + fun setUploadEventBaseUrl(uploadEventBaseUrl: String?) = + apply { analyticsInfo.uploadEventBaseUrl = uploadEventBaseUrl } + + fun setFgEventPeriodInSeconds(fgEventPeriodInSeconds: Long?) = + apply { analyticsInfo.fgEventPeriodInSeconds = fgEventPeriodInSeconds } + + fun setXAppId(xAppId: String) = + apply { analyticsInfo.xAppId = xAppId } + + fun setXDeviceInfo(xDeviceInfo: String) = + apply { analyticsInfo.xDeviceInfo = xDeviceInfo } + + fun setMainProcess(process: String) = + apply { analyticsInfo.mainProcess = process } + + fun build(): GuruAnalytics { + analyticsInfo.run { + INSTANCE.initialize( + context, + batchLimit, + uploadPeriodInSeconds, + startUploadDelayInSecond, + eventExpiredInDays, + debug, + persistableLog, + eventHandlerCallback, + isInitPeriodicWork, + uploadEventBaseUrl, + fgEventPeriodInSeconds, + xAppId, + xDeviceInfo, + mainProcess, + ) + } + return INSTANCE + } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/AnalyticsApi.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/AnalyticsApi.kt new file mode 100644 index 0000000..39c721a --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/AnalyticsApi.kt @@ -0,0 +1,23 @@ +package guru.core.analytics.data.api + +import io.reactivex.Single +import okhttp3.RequestBody +import retrofit2.Retrofit +import retrofit2.http.Body +import retrofit2.http.POST + +interface AnalyticsApi { + + @POST("event") + fun uploadEvent(@Body body: RequestBody): Single + + object Creator { + fun newInstance(retrofit: Retrofit): AnalyticsApi { + return retrofit.create(AnalyticsApi::class.java) + } + } +} + +object AnalyticsApiHost { + const val BASE_URL = "https://collect.saas.castbox.fm/" +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/GuruRepository.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/GuruRepository.kt new file mode 100644 index 0000000..0be4747 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/GuruRepository.kt @@ -0,0 +1,31 @@ +package guru.core.analytics.data.api + +import guru.core.analytics.utils.GZipUtils +import io.reactivex.Single +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * 数据获取汇总接口 + */ +interface GuruRepository { + var analyticsApi: AnalyticsApi + + fun uploadEvents(json: String): Single +} + +/** + * 数据调度具体实现 + */ +class DefaultGuruRepository( + override var analyticsApi: AnalyticsApi +) : GuruRepository { + + override fun uploadEvents(json: String): Single { + val gzipJson = GZipUtils.compress(json) + ?: return Single.error(IllegalArgumentException("param json is null or gzip fail")) + val requestBody = gzipJson.toRequestBody("application/json".toMediaTypeOrNull()) + return analyticsApi.uploadEvent(requestBody) + } + +} diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/ServiceLocator.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/ServiceLocator.kt new file mode 100644 index 0000000..ff0bdde --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/ServiceLocator.kt @@ -0,0 +1,247 @@ +package guru.core.analytics.data.api + +import android.content.Context +import android.os.SystemClock +import guru.core.analytics.data.api.logging.LoggingInterceptor +import guru.core.analytics.data.api.dns.CustomDns +import guru.core.analytics.data.api.dns.GoogleDnsApi +import guru.core.analytics.data.api.dns.GoogleDnsApiHost +import guru.core.analytics.data.api.logging.Level +import guru.core.analytics.data.local.PreferencesManager +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import guru.core.analytics.utils.AndroidUtils +import guru.core.analytics.utils.DateTimeUtils +import guru.core.analytics.utils.GsonUtil +import okhttp3.* +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import okhttp3.internal.platform.Platform +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +object ServiceLocator { + + private var debug = false + + @Volatile + private var guruRepository: GuruRepository? = null + + @Volatile + private var googleDnsApi: GoogleDnsApi? = null + + private val headerParams = mutableMapOf() + + fun addHeaderParam(key: String, value: String?) { + if (value.isNullOrBlank()) return + headerParams[key] = value + } + + fun setDebug(debug: Boolean) { + this.debug = debug + } + + fun provideGuruRepository(context: Context, baseUrl: String? = null): GuruRepository { + synchronized(this) { + return guruRepository + ?: createQuotesRepository(context, baseUrl).apply { guruRepository = this } + } + } + + private fun createQuotesRepository(context: Context, baseUrl: String? = null): GuruRepository { + return DefaultGuruRepository(provideAnalyticsApi(context, baseUrl)) + } + + private fun provideAnalyticsApi(context: Context, baseUrl: String? = null): AnalyticsApi { + val finalBaseUrl = if (!baseUrl.isNullOrEmpty()) { + baseUrl + } else { + val cacheBaseUrl = PreferencesManager.getInstance(context).uploadEventBaseUrl + if (cacheBaseUrl.isNullOrEmpty()) AnalyticsApiHost.BASE_URL else cacheBaseUrl + } + synchronized(this) { + return guruRepository?.analyticsApi ?: createAnalyticsApi(context, finalBaseUrl) + } + } + + private fun createAnalyticsApi(context: Context, baseUrl: String): AnalyticsApi { + return AnalyticsApi.Creator.newInstance( + Retrofit.Builder() + .baseUrl(baseUrl) + .client(createOkHttpClient(context)) + .addConverterFactory(createJsonConvertFactory()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) + .build() + ) + } + + fun updateAnalyticsBaseUrl(context: Context, baseUrl: String) { + if (baseUrl.isBlank()) return + guruRepository?.analyticsApi = createAnalyticsApi(context, baseUrl) + } + + fun provideGoogleDnsApi(context: Context): GoogleDnsApi { + synchronized(this) { + return googleDnsApi + ?: createGoogleDnsApi(context).apply { googleDnsApi = this } + } + } + + private fun createGoogleDnsApi(context: Context): GoogleDnsApi { + return GoogleDnsApi.Creator.newInstance( + Retrofit.Builder() + .baseUrl(GoogleDnsApiHost.API) + .client(createDnsOkHttpClient(context)) + .addConverterFactory(createJsonConvertFactory()) + .build() + ) + } + + private fun createJsonConvertFactory(): Converter.Factory { + return GsonConverterFactory.create(GsonUtil.gson) + } + + private fun createOkHttpClient( + context: Context, + readTimeOut: Long = 30L, + writeTimeOut: Long = 30L + ): OkHttpClient { + val builder = OkHttpClient.Builder() + .dispatcher(Dispatcher().apply { + maxRequests = 128 + maxRequestsPerHost = 10 + }) + .dns(CustomDns(context)) + .connectTimeout(20L, TimeUnit.SECONDS) + .readTimeout(readTimeOut, TimeUnit.SECONDS) + .writeTimeout(writeTimeOut, TimeUnit.SECONDS) + .addInterceptor(createCacheControlInterceptor(context)) + .addInterceptor(createAnalyticsApiInterceptor()) + .addInterceptor(createLoggingInterceptor()) + return builder.build() + } + + private fun createDnsOkHttpClient(context: Context): OkHttpClient { + val builder = OkHttpClient.Builder() + .addInterceptor(createCacheControlInterceptor(context)) + builder.addInterceptor(createLoggingInterceptor()) + return builder.build() + } + + private fun createLoggingInterceptor(): Interceptor { + return LoggingInterceptor.Builder() + .setLevel(Level.BASIC) + .log(Platform.INFO) + .request("AnalyticsRequest") + .response("AnalyticsResponse") + .build() + } + + private fun createCacheControlInterceptor(context: Context) = Interceptor { chain -> + try { + val originalResponse = chain.proceed(chain.request()) + when { + originalResponse.headers.names().contains("Cache-Control") -> { + return@Interceptor originalResponse.newBuilder().build() + } + AndroidUtils.isInternetAvailable(context) -> { + val maxAge = 60 * 5 // read from cache for 5 minute + return@Interceptor originalResponse.newBuilder() + .header("Cache-Control", "public, max-age=$maxAge") + .build() + } + else -> { + val maxStale = 60 * 60 * 24 * 28 // tolerate 4-weeks stale + return@Interceptor originalResponse.newBuilder() + .header("Cache-Control", "public, only-if-cached, max-stale=$maxStale") + .build() + } + } + } catch (e: Exception) { + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_CACHE_CONTROL, e.message) + return@Interceptor exceptionResponse( + e, + chain.request() + ) + } + } + + private fun createAnalyticsApiInterceptor(): Interceptor { + return Interceptor { chain -> + val request = chain.request() + val startTime = SystemClock.elapsedRealtime() + val builder = request.newBuilder() + .addHeader(CONTENT_TYPE, "application/json") + .addHeader(CONTENT_ENCODING, "gzip") + .addHeader(X_EVENT_TYPE, "event") + headerParams.forEach { + builder.addHeader(it.key, it.value) + } + val newRequest = builder.build() + try { + val response = chain.proceed(newRequest) + val responseTime = (SystemClock.elapsedRealtime() - startTime) / 2 + calibrationTime(responseTime, response.headers) + if (response.isSuccessful) { + successResponse(response) + } else { + httpErrorResponse(response) + } + } catch (e: Exception) { + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_API, e.message) + return@Interceptor exceptionResponse(e, newRequest) + } + } + } + + private var minResponseTime: Long = Long.MAX_VALUE + private fun calibrationTime(responseTime: Long, headers: Headers?) { + // If the current response time is less than the previous min one, calibrate again + if (headers == null || responseTime >= minResponseTime) return + val date = headers.getDate("Date") ?: return + DateTimeUtils.initServerTime(date.time + responseTime) + minResponseTime = responseTime + } + + private fun successResponse(response: Response): Response { + val body: ResponseBody? + var bodyString: String? + response.body.let { + bodyString = it?.string() + body = bodyString?.toResponseBody(it!!.contentType()) + } + return response.newBuilder() + .body(body) + .build() + } + + private fun httpErrorResponse(response: Response): Response { + val body = + "{\"code\": ${response.code}, \"msg\": \"${response.message}\", \"data\": null}".toResponseBody() + response.close() + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_RESPONSE, response.code.toString()) + return response.newBuilder() + .code(1) + .body(body) + .build() + } + + private fun exceptionResponse(e: Exception, request: Request): Response { + val content = "{\"code\": -1, \"msg\": \"${e.message}\", \"data\": null}" + return Response.Builder() + .code(1) + .request(request) + .protocol(Protocol.HTTP_1_1) + .message("${e.message}") + .body(content.toResponseBody()) + .build() + } +} + +const val CONTENT_TYPE = "Content-Type" +const val CONTENT_ENCODING = "Content-Encoding" +const val X_EVENT_TYPE = "x_event_type" + diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/dns/CustomDns.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/dns/CustomDns.kt new file mode 100644 index 0000000..1036908 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/dns/CustomDns.kt @@ -0,0 +1,53 @@ +package guru.core.analytics.data.api.dns + +import android.content.Context +import guru.core.analytics.data.api.ServiceLocator +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import kotlinx.coroutines.runBlocking +import okhttp3.Dns +import java.net.InetAddress +import java.net.UnknownHostException + +class CustomDns(private val context: Context) : Dns { + + override fun lookup(hostname: String): List { + return try { + Dns.SYSTEM.lookup(hostname) + } catch (e: UnknownHostException) { + try { + lookupByGoogleDns(hostname) + } catch (e: UnknownHostException) { + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_DNS, e.message) + lookupByRemoteConfig(hostname) + } + } + } + + private fun lookupByGoogleDns(hostname: String): List { + val dnsApi = ServiceLocator.provideGoogleDnsApi(context) + val ipList = runBlocking { + return@runBlocking dnsApi.ip(hostname).answer?.toMutableList() + ?.filter { it.type == 1 } + ?.mapNotNull { it.data } + } + if (!ipList.isNullOrEmpty()) { + return ipList.map { InetAddress.getByAddress(convert(it)) } + } + throw UnknownHostException("Broken Google dns lookup of $hostname") + } + + private fun lookupByRemoteConfig(hostname: String): List { +// val dnsConfig = RemoteConfig.getDnsConfig() +// val ipList = dnsConfig?.get(hostname) +// if (ipList != null && ipList.isNotEmpty()) { +// return ipList.map { InetAddress.getByAddress(convert(it)) } +// } + throw UnknownHostException("Broken remote config lookup of $hostname") + } + + private fun convert(ip: String): ByteArray { + val ipArray = ip.split(".").map { Integer.parseInt(it).toByte() } + return ipArray.toByteArray() + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/dns/GoogleDnsApi.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/dns/GoogleDnsApi.kt new file mode 100644 index 0000000..449a72e --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/dns/GoogleDnsApi.kt @@ -0,0 +1,37 @@ +package guru.core.analytics.data.api.dns + +import com.google.gson.annotations.SerializedName +import retrofit2.Retrofit +import retrofit2.http.GET +import retrofit2.http.Query + +/** + * Api endpoint: https://dns.google.com/ + */ +@Suppress("unused") +interface GoogleDnsApi { + + @GET("resolve") + suspend fun ip(@Query("name") name: String): IpResponse + + object Creator { + fun newInstance(retrofit: Retrofit): GoogleDnsApi { + return retrofit.create(GoogleDnsApi::class.java) + } + } +} + +data class IpResponse( + @SerializedName("Answer") val answer: List? = null +) + +data class Answer( + @SerializedName("name") val name: String? = null, + @SerializedName("type") val type: Int? = null, + @SerializedName("TTL") val ttl: Int? = null, + @SerializedName("data") val data: String? = null +) + +object GoogleDnsApiHost { + const val API = "https://dns.google.com/" +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/BufferListener.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/BufferListener.kt new file mode 100644 index 0000000..455b428 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/BufferListener.kt @@ -0,0 +1,12 @@ +package guru.core.analytics.data.api.logging + +import okhttp3.Request +import java.io.IOException + +/** + * @author ihsan on 8/12/18. + */ +interface BufferListener { + @Throws(IOException::class) + fun getJsonResponse(request: Request?): String? +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/I.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/I.kt new file mode 100644 index 0000000..7346efb --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/I.kt @@ -0,0 +1,36 @@ +package guru.core.analytics.data.api.logging + +import okhttp3.internal.platform.Platform.Companion.INFO +import java.util.logging.Level +import java.util.logging.Logger + +/** + * @author ihsan on 10/02/2017. + */ +internal open class I protected constructor() { + companion object { + private val prefix = arrayOf(". ", " .") + private var index = 0 + fun log(type: Int, tag: String, msg: String?, isLogHackEnable: Boolean) { + val finalTag = getFinalTag(tag, isLogHackEnable) + val logger = Logger.getLogger(if (isLogHackEnable) finalTag else tag) + when (type) { + INFO -> logger.log(Level.INFO, msg) + else -> logger.log(Level.WARNING, msg) + } + } + + private fun getFinalTag(tag: String, isLogHackEnable: Boolean): String { + return if (isLogHackEnable) { + index = index xor 1 + prefix[index] + tag + } else { + tag + } + } + } + + init { + throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Level.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Level.kt new file mode 100644 index 0000000..269b5a7 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Level.kt @@ -0,0 +1,39 @@ +package guru.core.analytics.data.api.logging + +/** + * @author ihsan on 21/02/2017. + */ +enum class Level { + /** + * No logs. + */ + NONE, + /** + * + * Example: + *
`- URL
+     * - Method
+     * - Headers
+     * - Body
+    `
* + */ + BASIC, + /** + * + * Example: + *
`- URL
+     * - Method
+     * - Headers
+    `
* + */ + HEADERS, + /** + * + * Example: + *
`- URL
+     * - Method
+     * - Body
+    `
* + */ + BODY +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Logger.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Logger.kt new file mode 100644 index 0000000..e4d81fb --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Logger.kt @@ -0,0 +1,19 @@ +package guru.core.analytics.data.api.logging + +import okhttp3.internal.platform.Platform +import okhttp3.internal.platform.Platform.Companion.INFO + +/** + * @author ihsan on 11/07/2017. + */ +interface Logger { + fun log(level: Int = INFO, tag: String?= null, msg: String? = null) + + companion object { + val DEFAULT: Logger = object : Logger { + override fun log(level: Int, tag: String?, msg: String?) { + Platform.get().log("$msg", level, null) + } + } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/LoggingInterceptor.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/LoggingInterceptor.kt new file mode 100644 index 0000000..52d6c79 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/LoggingInterceptor.kt @@ -0,0 +1,263 @@ +package guru.core.analytics.data.api.logging + +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.ResponseBody.Companion.toResponseBody +import okhttp3.internal.platform.Platform.Companion.INFO +import java.util.* +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit + + +/** + * @author ihsan on 09/02/2017. + */ +class LoggingInterceptor private constructor(private val builder: Builder) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = addQueryAndHeaders(chain.request()) + + if (builder.level == Level.NONE) { + return chain.proceed(request) + } + + printlnRequestLog(request) + + val startNs = System.nanoTime() + val response: Response + try { + response = proceedResponse(chain, request) + } catch (e: Exception) { + Printer.printFailed(builder.getTag(false), builder) + throw e + } + val receivedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) + + printlnResponseLog(receivedMs, response, request) + return response + } + + private fun printlnResponseLog(receivedMs: Long, response: Response, request: Request) { + Printer.printJsonResponse( + builder, + receivedMs, + response.isSuccessful, + response.code, + response.headers, + response, + request.url.encodedPathSegments, + response.message, + request.url.toString() + ) + } + + private fun printlnRequestLog(request: Request) { + Printer.printJsonRequest( + builder, + request.body, + request.url.toUrl().toString(), + request.headers, + request.method + ) + } + + private fun proceedResponse(chain: Interceptor.Chain, request: Request): Response { + return if (builder.isMockEnabled && builder.listener != null) { + TimeUnit.MILLISECONDS.sleep(builder.sleepMs) + Response.Builder() + .body(builder.listener!!.getJsonResponse(request)?.toResponseBody("application/json".toMediaTypeOrNull())) + .request(chain.request()) + .protocol(Protocol.HTTP_2) + .message("Mock data from LoggingInterceptor") + .code(200) + .build() + } else chain.proceed(request) + } + + private fun addQueryAndHeaders(request: Request): Request { + val requestBuilder = request.newBuilder() + builder.headers.keys.forEach { key -> + builder.headers[key]?.let { + requestBuilder.addHeader(key, it) + } + } + val httpUrlBuilder: HttpUrl.Builder? = request.url.newBuilder(request.url.toString()) + httpUrlBuilder?.let { + builder.httpUrl.keys.forEach { key -> + httpUrlBuilder.addQueryParameter(key, builder.httpUrl[key]) + } + } + return requestBuilder.url(httpUrlBuilder?.build()!!).build() + } + + @Suppress("unused") + class Builder { + val headers: HashMap = HashMap() + val httpUrl: HashMap = HashMap() + var isLogHackEnable = false + private set + var isDebugAble = false + var type: Int = INFO + private set + private var requestTag: String? = null + private var responseTag: String? = null + var level = Level.BASIC + private set + var logger: Logger? = null + private set + var isMockEnabled = false + var sleepMs: Long = 0 + var listener: BufferListener? = null + + /** + * @param level set log level + * @return Builder + * @see Level + */ + fun setLevel(level: Level): Builder { + this.level = level + return this + } + + fun getTag(isRequest: Boolean): String { + return when (isRequest) { + true -> if (requestTag.isNullOrEmpty()) TAG else requestTag!! + false -> if (responseTag.isNullOrEmpty()) TAG else responseTag!! + } + } + + /** + * @param name Filed + * @param value Value + * @return Builder + * Add a field with the specified value + */ + fun addHeader(name: String, value: String): Builder { + headers[name] = value + return this + } + + /** + * @param name Filed + * @param value Value + * @return Builder + * Add a field with the specified value + */ + fun addQueryParam(name: String, value: String): Builder { + httpUrl[name] = value + return this + } + + /** + * Set request and response each log tag + * + * @param tag general log tag + * @return Builder + */ + fun tag(tag: String): Builder { + TAG = tag + return this + } + + /** + * Set request log tag + * + * @param tag request log tag + * @return Builder + */ + fun request(tag: String?): Builder { + requestTag = tag + return this + } + + /** + * Set response log tag + * + * @param tag response log tag + * @return Builder + */ + fun response(tag: String?): Builder { + responseTag = tag + return this + } + + /** + * @param isDebug set can sending log output + * @return Builder + */ + @Deprecated(message = "Set level based on your requirement", + replaceWith = ReplaceWith(expression = "setLevel(Level.Basic)"), + level = DeprecationLevel.ERROR) + fun loggable(isDebug: Boolean): Builder { + this.isDebugAble = isDebug + return this + } + + /** + * @param type set sending log output type + * @return Builder + * @see okhttp3.internal.platform.Platform + */ + fun log(type: Int): Builder { + this.type = type + return this + } + + /** + * @param logger manuel logging interface + * @return Builder + * @see Logger + */ + fun logger(logger: Logger?): Builder { + this.logger = logger + return this + } + + /** + * @param executor manual executor for printing + * @return Builder + * @see Logger + */ + @Deprecated(message = "Create your own Logcat filter for best result", level = DeprecationLevel.ERROR) + fun executor(executor: Executor?): Builder { + TODO("Deprecated") + } + + /** + * @param useMock let you use json file from asset + * @param sleep let you see progress dialog when you request + * @return Builder + * @see LoggingInterceptor + */ + fun enableMock(useMock: Boolean, sleep: Long, listener: BufferListener?): Builder { + isMockEnabled = useMock + sleepMs = sleep + this.listener = listener + return this + } + + /** + * Call this if you want to have formatted pretty output in Android Studio logCat. + * By default this 'hack' is not applied. + * + * @param useHack setup builder to use hack for Android Studio v3+ in order to have nice + * output as it was in previous A.S. versions. + * @return Builder + * @see Logger + */ + @Deprecated(message = "Android studio has resolved problem for latest versions", + level = DeprecationLevel.WARNING) + fun enableAndroidStudioV3LogsHack(useHack: Boolean): Builder { + isLogHackEnable = useHack + return this + } + + fun build(): LoggingInterceptor { + return LoggingInterceptor(this) + } + + companion object { + private var TAG = "LoggingI" + } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Printer.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Printer.kt new file mode 100644 index 0000000..7104c49 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/api/logging/Printer.kt @@ -0,0 +1,273 @@ +package guru.core.analytics.data.api.logging + +import okhttp3.Headers +import okhttp3.RequestBody +import okhttp3.Response +import okhttp3.internal.http.promisesBody +import okio.Buffer +import okio.GzipSource +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.EOFException +import java.io.IOException +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +/** + * @author ihsan on 09/02/2017. + */ +class Printer private constructor() { + companion object { + private const val JSON_INDENT = 3 + private val LINE_SEPARATOR = System.getProperty("line.separator") + private val DOUBLE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR + private const val N = "\n" + private const val T = "\t" + private const val REQUEST_UP_LINE = "┌────── Request ────────────────────────────────────────────────────────────────────────" + private const val END_LINE = "└───────────────────────────────────────────────────────────────────────────────────────" + private const val RESPONSE_UP_LINE = "┌────── Response ───────────────────────────────────────────────────────────────────────" + private const val BODY_TAG = "Body:" + private const val URL_TAG = "URL: " + private const val METHOD_TAG = "Method: @" + private const val HEADERS_TAG = "Headers:" + private const val STATUS_CODE_TAG = "Status Code: " + private const val RECEIVED_TAG = "Received in: " + private const val DEFAULT_LINE = "│ " + private val OOM_OMITTED = LINE_SEPARATOR + "Output omitted because of Object size." + private fun isEmpty(line: String): Boolean { + return line.isEmpty() || N == line || T == line || line.trim { it <= ' ' }.isEmpty() + } + + fun printJsonRequest(builder: LoggingInterceptor.Builder, body: RequestBody?, url: String, header: Headers, method: String) { + val requestBody = body?.let { + LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + bodyToString(body, header) + } ?: "" + val tag = builder.getTag(true) + if (builder.logger == null) I.log(builder.type, tag, REQUEST_UP_LINE, builder.isLogHackEnable) + logLines(builder.type, tag, arrayOf(URL_TAG + url), builder.logger, false, builder.isLogHackEnable) + logLines(builder.type, tag, getRequest(builder.level, header, method), builder.logger, true, builder.isLogHackEnable) + if (builder.level == Level.BASIC || builder.level == Level.BODY) { + logLines(builder.type, tag, requestBody.split(LINE_SEPARATOR).toTypedArray(), builder.logger, true, builder.isLogHackEnable) + } + if (builder.logger == null) I.log(builder.type, tag, END_LINE, builder.isLogHackEnable) + } + + fun printJsonResponse(builder: LoggingInterceptor.Builder, chainMs: Long, isSuccessful: Boolean, + code: Int, headers: Headers, response: Response, segments: List, message: String, responseUrl: String) { + val responseBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + getResponseBody(response) + val tag = builder.getTag(false) + val urlLine = arrayOf(URL_TAG + responseUrl, N) + val responseString = getResponse(headers, chainMs, code, isSuccessful, + builder.level, segments, message) + if (builder.logger == null) { + I.log(builder.type, tag, RESPONSE_UP_LINE, builder.isLogHackEnable) + } + logLines(builder.type, tag, urlLine, builder.logger, true, builder.isLogHackEnable) + logLines(builder.type, tag, responseString, builder.logger, true, builder.isLogHackEnable) + if (builder.level == Level.BASIC || builder.level == Level.BODY) { + logLines(builder.type, tag, responseBody.split(LINE_SEPARATOR).toTypedArray(), builder.logger, + true, builder.isLogHackEnable) + } + if (builder.logger == null) { + I.log(builder.type, tag, END_LINE, builder.isLogHackEnable) + } + } + + private fun getResponseBody(response: Response): String { + val responseBody = response.body!! + val headers = response.headers + val contentLength = responseBody.contentLength() + if (!response.promisesBody()) { + return "End request - Promises Body" + } else if (bodyHasUnknownEncoding(response.headers)) { + return "encoded body omitted" + } else { + val source = responseBody.source() + source.request(Long.MAX_VALUE) // Buffer the entire body. + var buffer = source.buffer + + var gzippedLength: Long? = null + if ("gzip".equals(headers["Content-Encoding"], ignoreCase = true)) { + gzippedLength = buffer.size + GzipSource(buffer.clone()).use { gzippedResponseBody -> + buffer = Buffer() + buffer.writeAll(gzippedResponseBody) + } + } + + val contentType = responseBody.contentType() + val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) + ?: StandardCharsets.UTF_8 + + if (!buffer.isProbablyUtf8()) { + return "End request - binary ${buffer.size}:byte body omitted" + } + + if (contentLength != 0L) { + return getJsonString(buffer.clone().readString(charset)) + } + + return if (gzippedLength != null) { + "End request - ${buffer.size}:byte, $gzippedLength-gzipped-byte body" + } else { + "End request - ${buffer.size}:byte body" + } + } + } + + private fun getRequest(level: Level, headers: Headers, method: String): Array { + val log: String + val loggableHeader = level == Level.HEADERS || level == Level.BASIC + log = METHOD_TAG + method + DOUBLE_SEPARATOR + + if (isEmpty("$headers")) "" else if (loggableHeader) HEADERS_TAG + LINE_SEPARATOR + dotHeaders(headers) else "" + return log.split(LINE_SEPARATOR).toTypedArray() + } + + private fun getResponse(headers: Headers, tookMs: Long, code: Int, isSuccessful: Boolean, + level: Level, segments: List, message: String): Array { + val log: String + val loggableHeader = level == Level.HEADERS || level == Level.BASIC + val segmentString = slashSegments(segments) + log = ((if (segmentString.isNotEmpty()) "$segmentString - " else "") + "[is success : " + + isSuccessful + "] - " + RECEIVED_TAG + tookMs + "ms" + DOUBLE_SEPARATOR + STATUS_CODE_TAG + + code + " / " + message + DOUBLE_SEPARATOR + when { + isEmpty("$headers") -> "" + loggableHeader -> HEADERS_TAG + LINE_SEPARATOR + + dotHeaders(headers) + else -> "" + }) + return log.split(LINE_SEPARATOR).toTypedArray() + } + + private fun slashSegments(segments: List): String { + val segmentString = StringBuilder() + for (segment in segments) { + segmentString.append("/").append(segment) + } + return segmentString.toString() + } + + private fun dotHeaders(headers: Headers): String { + val builder = StringBuilder() + headers.forEach { pair -> + builder.append("${pair.first}: ${pair.second}").append(N) + } + return builder.dropLast(1).toString() + } + + private fun logLines(type: Int, tag: String, lines: Array, logger: Logger?, + withLineSize: Boolean, useLogHack: Boolean) { + for (line in lines) { + val lineLength = line.length + val maxLogSize = if (withLineSize) 110 else lineLength + for (i in 0..lineLength / maxLogSize) { + val start = i * maxLogSize + var end = (i + 1) * maxLogSize + end = if (end > line.length) line.length else end + if (logger == null) { + I.log(type, tag, DEFAULT_LINE + line.substring(start, end), useLogHack) + } else { + logger.log(type, tag, line.substring(start, end)) + } + } + } + } + + private fun bodyToString(requestBody: RequestBody?, headers: Headers): String { + return requestBody?.let { + return try { + when { + bodyHasUnknownEncoding(headers) -> { + return "encoded body omitted)" + } + requestBody.isDuplex() -> { + return "duplex request body omitted" + } + requestBody.isOneShot() -> { + return "one-shot body omitted" + } + else -> { + val buffer = Buffer() + requestBody.writeTo(buffer) + + val contentType = requestBody.contentType() + val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) + ?: StandardCharsets.UTF_8 + + return if (buffer.isProbablyUtf8()) { + getJsonString(buffer.readString(charset)) + LINE_SEPARATOR + "${requestBody.contentLength()}-byte body" + } else { + "binary ${requestBody.contentLength()}-byte body omitted" + } + } + } + } catch (e: IOException) { + "{\"err\": \"" + e.message + "\"}" + } + } ?: "" + } + + private fun bodyHasUnknownEncoding(headers: Headers): Boolean { + val contentEncoding = headers["Content-Encoding"] ?: return false + return !contentEncoding.equals("identity", ignoreCase = true) && + !contentEncoding.equals("gzip", ignoreCase = true) + } + + private fun getJsonString(msg: String): String { + val message: String = try { + when { + msg.startsWith("{") -> { + val jsonObject = JSONObject(msg) + jsonObject.toString(JSON_INDENT) + } + msg.startsWith("[") -> { + val jsonArray = JSONArray(msg) + jsonArray.toString(JSON_INDENT) + } + else -> { + msg + } + } + } catch (e: JSONException) { + msg + } catch (e1: OutOfMemoryError) { + OOM_OMITTED + } + return message + } + + fun printFailed(tag: String, builder: LoggingInterceptor.Builder) { + I.log(builder.type, tag, RESPONSE_UP_LINE, builder.isLogHackEnable) + I.log(builder.type, tag, DEFAULT_LINE + "Response failed", builder.isLogHackEnable) + I.log(builder.type, tag, END_LINE, builder.isLogHackEnable) + } + } + + init { + throw UnsupportedOperationException() + } +} + +/** + * @see 'https://github.com/square/okhttp/blob/master/okhttp-logging-interceptor/src/main/java/okhttp3/logging/utf8.kt' + * */ +internal fun Buffer.isProbablyUtf8(): Boolean { + try { + val prefix = Buffer() + val byteCount = size.coerceAtMost(64) + copyTo(prefix, 0, byteCount) + for (i in 0 until 16) { + if (prefix.exhausted()) { + break + } + val codePoint = prefix.readUtf8CodePoint() + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { + return false + } + } + return true + } catch (_: EOFException) { + return false // Truncated UTF-8 sequence. + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/GuruAnalyticsDatabase.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/GuruAnalyticsDatabase.kt new file mode 100644 index 0000000..f93f51e --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/GuruAnalyticsDatabase.kt @@ -0,0 +1,77 @@ +package guru.core.analytics.data.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.sqlite.db.SupportSQLiteDatabase +import guru.core.analytics.data.db.dao.EventDao +import guru.core.analytics.data.db.migrations.MIGRATIONS +import guru.core.analytics.data.db.model.EventEntity +import guru.core.analytics.data.db.utils.Converters +import guru.core.analytics.data.db.utils.TransactionResult +import guru.core.analytics.data.db.utils.runInTransactionEx +import io.reactivex.Maybe +import timber.log.Timber +import java.lang.ref.SoftReference + +@Database( + entities = [ + EventEntity::class, + ], + version = 1, + exportSchema = false, +) +@TypeConverters(Converters::class) +abstract class GuruAnalyticsDatabase : RoomDatabase() { + abstract fun eventDao(): EventDao + + companion object { + + private lateinit var appContext: SoftReference + + private val context + get() = appContext.get()!! + + private const val dbName = "guru_analytics" + + @Volatile + private var INSTANCE: GuruAnalyticsDatabase? = null + + @Synchronized + fun initialize(context: Context) { + if (INSTANCE == null) { + appContext = SoftReference(context.applicationContext) + INSTANCE = newInstance() + } + } + + fun getInstance(): GuruAnalyticsDatabase = INSTANCE!! + + private fun newInstance(): GuruAnalyticsDatabase { + return Room.databaseBuilder(context, GuruAnalyticsDatabase::class.java, dbName) + .addMigrations(*MIGRATIONS) + .addCallback(object : RoomDatabase.Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + Timber.d("database onCreate") + super.onCreate(db) + } + + override fun onOpen(db: SupportSQLiteDatabase) { + Timber.d("database onOpen") + super.onOpen(db) + } + }) + .build() + } + + fun runInTransaction(callback: () -> TransactionResult, defVal: T?): Maybe { + return INSTANCE?.runInTransactionEx(callback, defVal) ?: Maybe.empty() + } + + fun runInTransaction(callback: () -> TransactionResult): Maybe { + return INSTANCE?.runInTransactionEx(callback) ?: Maybe.empty() + } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/dao/EventDao.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/dao/EventDao.kt new file mode 100644 index 0000000..d521685 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/dao/EventDao.kt @@ -0,0 +1,91 @@ +package guru.core.analytics.data.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import guru.core.analytics.data.db.model.EventEntity +import guru.core.analytics.data.store.EventInfoStore + +@Dao +abstract class EventDao { + + companion object { + private const val TAG = "EventDao" + } + + fun updateEventUploading(events: List) { + val ids = getIds(events) + updateEventState(1, EventInfoStore.SESSION, ids) + } + + fun updateEventDefault(events: List) { + val ids = getIds(events) + updateEventState(0, EventInfoStore.SESSION, ids) + } + + private fun getIds(events: List) = events.map { it.id }.toTypedArray() + + @Transaction + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun addEvent(event: EventEntity) + + @Transaction + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun addEvents(events: List) + + @Query("SELECT * FROM Event WHERE status = 0 ORDER BY priority ASC, at ASC LIMIT :limit") + abstract fun getEvents(limit: Int): List + + @Query("SELECT * FROM Event WHERE status = 0 ORDER BY priority ASC, at ASC") + abstract fun getAllEvents(): List + + @Query("UPDATE Event SET status = :status, session = :session WHERE id in (:keys)") + abstract fun updateEventState(status: Int, session: String, keys: Array) + + @Transaction + open fun loadAndMarkAllUploadEvents(): List { + val events = getAllEvents() + updateEventUploading(events) + return events + } + + @Transaction + open fun loadAndMarkUploadEvents(limit: Int): List { + val events = getEvents(limit).toMutableList() +// if (events.isNotEmpty()) { +// Timber.tag(TAG).d("loadAndMarkUploadEvents limit:$limit size:${events.size}") +// FgEventHelper.getInstance().getFgEvent()?.let { entity -> +// addEvent(entity) +// events.add(entity) +// } +// } + updateEventUploading(events) + return events + } + + @Transaction + open fun deleteExpiredEvents(timestamp: Long): Pair { + val deletedEventsCount = deleteEvents(timestamp) + val resetEventsCount = resetEventStateExceptSession(EventInfoStore.SESSION) + return deletedEventsCount to resetEventsCount + } + + @Query("UPDATE Event SET status = :status") + abstract fun updateEventState(status: Int): Int + + @Query("UPDATE Event SET status = 0 WHERE session != :exceptSession") + abstract fun resetEventStateExceptSession(exceptSession: String): Int + + @Transaction + @Delete + abstract fun deleteEvents(events: List) + + @Query("DELETE FROM Event WHERE at < :timestamp") + abstract fun deleteEvents(timestamp: Long): Int + + @Query("SELECT count(id) FROM Event WHERE status = 0") + abstract fun getEventCount(): LiveData + + @Transaction + @Query("DELETE FROM Event WHERE (SELECT count(id) FROM Event) > :max AND at IN (SELECT at FROM Event ORDER BY at DESC LIMIT(SELECT count(id) FROM Event) OFFSET :max)") + abstract fun deleteExceedEvents(max: Int) +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/migrations/Migrations.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/migrations/Migrations.kt new file mode 100644 index 0000000..af9f76f --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/migrations/Migrations.kt @@ -0,0 +1,16 @@ +package guru.core.analytics.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import timber.log.Timber + +/** + * Created by Haoyi on 2022-11-05. + */ +private val MIGRATION_1_TO_2: Migration = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + Timber.d("migrate 1 to 2") + } +} + +val MIGRATIONS = emptyArray() \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/Event.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/Event.kt new file mode 100644 index 0000000..6971f65 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/Event.kt @@ -0,0 +1,41 @@ +package guru.core.analytics.data.db.model + +import androidx.annotation.IntDef +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + + +@Keep +data class Event( + @SerializedName("timestamp") val timestamp: Long, // 客户端中记录此事件的时间(采用世界协调时间,毫秒为单位) + @SerializedName("event") val event: String, // 事件名称 + @SerializedName("info") val info: Map?, // 包含deviceId / uid / adjustId / adId / firebaseId 等 + @SerializedName("param") val param: Map?, // 事件参数 + @SerializedName("properties") val properties: Map?, // 用户属性信息 + @SerializedName("eventId") val eventId: String, +) + +@Keep +data class ParamValue( + @SerializedName("s") val s: String? = null, // 事件参数的字符串值 + @SerializedName("i") val i: Long? = null, // 事件参数的整数值 + @SerializedName("d") val d: Double? = null, // 事件参数的小数值。注意:APP序列化成JSON时,注意不要序列化成科学计数法 +) + +@IntDef(EventPriority.EMERGENCE, EventPriority.HIGH, EventPriority.DEFAULT, EventPriority.LOW) +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.FIELD, + AnnotationTarget.LOCAL_VARIABLE, + AnnotationTarget.CLASS +) +annotation class EventPriority { + companion object { + const val EMERGENCE = 0 + const val HIGH = 5 + const val DEFAULT = 10 + const val LOW = 15 + } +} diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventEntity.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventEntity.kt new file mode 100644 index 0000000..b69f90a --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventEntity.kt @@ -0,0 +1,37 @@ +package guru.core.analytics.data.db.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "Event") +data class EventEntity( + @PrimaryKey + @ColumnInfo(name = "id") + val id: String, + + @ColumnInfo(name = "session") + val session: String, + + @ColumnInfo(name = "json") + var json: String, + + @ColumnInfo(name = "ext") + val ext: String, + + @ColumnInfo(name = "priority", defaultValue = "${EventPriority.DEFAULT}") + val priority: Int, + + @ColumnInfo(name = "status") + val status: Int, + + @ColumnInfo(name = "at") + val at: Long, + + @ColumnInfo(name = "event") + val event: String, + + @ColumnInfo(name = "version") + val version: Int, + + ) \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventStatistic.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventStatistic.kt new file mode 100644 index 0000000..058460e --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/model/EventStatistic.kt @@ -0,0 +1,7 @@ +package guru.core.analytics.data.db.model + +data class EventStatistic( + val eventCountAll: Int = 0, + val eventCountDeleted: Int = 0, + val eventCountUploaded: Int = 0, +) \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Converters.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Converters.kt new file mode 100644 index 0000000..d97e917 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Converters.kt @@ -0,0 +1,19 @@ +package guru.core.analytics.data.db.utils + +import androidx.room.TypeConverter +import java.util.* + +/** + * Created by Haoyi on 2019/1/11. + */ +class Converters { + @TypeConverter + fun fromTimestamp(value: Long?): Date? { + return value?.let { Date(it) } + } + + @TypeConverter + fun dateToTimestamp(date: Date?): Long? { + return date?.let { it.time } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/DatabaseException.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/DatabaseException.kt new file mode 100644 index 0000000..b313d30 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/DatabaseException.kt @@ -0,0 +1,7 @@ +package guru.core.analytics.data.db.utils + +/** + * Created by Haoyi on 2022-11-05. + */ +class DatabaseException(message: String, cause: Throwable? = null) : Exception(message, cause) { +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Transactions.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Transactions.kt new file mode 100644 index 0000000..857b026 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/db/utils/Transactions.kt @@ -0,0 +1,60 @@ +package guru.core.analytics.data.db.utils + +import androidx.room.RoomDatabase +import io.reactivex.Maybe + +/** + * Created by Haoyi on 2022-11-05. + */ +enum class ResultBehavior { + SUCCESS, IGNORE, ERROR +} + +data class TransactionResult( + val value: T?, + val behavior: ResultBehavior, + val cause: Throwable? = null +) { + companion object { + fun obtainIgnoreResult(): TransactionResult { + return TransactionResult(null, ResultBehavior.IGNORE) + } + + fun obtainSuccessResult(value: R? = null): TransactionResult { + return TransactionResult(value, ResultBehavior.SUCCESS) + } + + fun obtainErrorResult(throwable: Throwable? = null): TransactionResult { + return TransactionResult(null, ResultBehavior.ERROR, throwable) + } + } +} + +fun RoomDatabase.runInTransactionEx( + callback: () -> TransactionResult, + defVal: T? +): Maybe { + return Maybe.create { emitter -> + val result = this.runInTransaction(callback) + ?: TransactionResult(defVal, ResultBehavior.SUCCESS) + when (result.behavior) { + ResultBehavior.SUCCESS -> { + val value = result.value + if (value != null) { + emitter.onSuccess(value) + } + emitter.onComplete() + } + ResultBehavior.ERROR -> { + emitter.onError(DatabaseException("runInTransaction error!", result.cause)) + } + else -> { + emitter.onComplete() + } + } + } +} + +fun RoomDatabase.runInTransactionEx(callback: () -> TransactionResult): Maybe { + return runInTransactionEx(callback, null) +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/Clearable.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/Clearable.kt new file mode 100644 index 0000000..51ae49e --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/Clearable.kt @@ -0,0 +1,11 @@ +package guru.core.analytics.data.local + +import kotlin.reflect.KProperty + +/** + * Created by Haoyi on 2017/12/20. + */ +interface Clearable { + fun clear(thisRef: PreferenceHolder, property: KProperty<*>) + fun clearCache() +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceCache.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceCache.kt new file mode 100644 index 0000000..4b8710c --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceCache.kt @@ -0,0 +1,10 @@ +package guru.core.analytics.data.local + +import android.util.LruCache + +/** + * Created by Haoyi on 2018/6/5. + */ +object PreferenceCache : LruCache(32) { + override fun sizeOf(key: String?, value: Any?): Int = 1 +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceFieldDelegate.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceFieldDelegate.kt new file mode 100644 index 0000000..49625b4 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceFieldDelegate.kt @@ -0,0 +1,91 @@ +package guru.core.analytics.data.local + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.util.Log +import guru.core.analytics.data.local.PreferenceHolder.Companion.CACHE +import guru.core.analytics.data.local.PreferenceHolder.Companion.SCHEDULER +import timber.log.Timber +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KClass +import kotlin.reflect.KProperty + +/** + * Created by Haoyi on 2017/12/13. + */ +internal class PreferenceFieldDelegate( + private val clazz: KClass, + private val key: String, + private val default: () -> T? +) : ReadWriteProperty, Clearable { + override fun getValue(thisRef: PreferenceHolder, property: KProperty<*>): T? = + readValue(thisRef, property).apply { field = this } + + override fun setValue(thisRef: PreferenceHolder, property: KProperty<*>, value: T?) { + field = value + saveNewValue(thisRef, property, value) + } + + override fun clear(thisRef: PreferenceHolder, property: KProperty<*>) { + setValue(thisRef, property, null) + } + + override fun clearCache() { + field = null + } + + var field: T? = null + + private fun saveNewValue(thisRef: PreferenceHolder, property: KProperty<*>, value: T?) { + if (value != null) CACHE.put(key, value) else CACHE.remove(key) + thisRef.getSharedPreferences() + .observeOn(SCHEDULER) + .subscribe( + { + if (value == null) { + removeValue(thisRef) + } else { + it.edit().apply { putValue(clazz, value, key) }.apply() + } + }, { + it.printStackTrace() + Log.d("Preference", "====> zhy saveNewValue error!", it) + } + ) + } + + @Suppress("UNCHECKED_CAST") + private fun readValue(thisRef: PreferenceHolder, property: KProperty<*>): T? { + val result = CACHE.get(key) + return try { + when { + clazz.isInstance(result) -> result as T + else -> thisRef.getSharedPreferencesDirectly().getValue(property).value + } + } catch (err: Throwable) { + default() + } + } + + private inner class Result(val value: T?) + + private fun SharedPreferences.getValue(property: KProperty<*>): Result { + val value = if (contains(key)) { + getFromPreference(clazz, default(), key) + } else { + default() + } + return Result(value) + } + + @SuppressLint("ApplySharedPref") + private fun removeValue(thisRef: PreferenceHolder) { + CACHE.remove(key) + thisRef.getSharedPreferences() + .observeOn(SCHEDULER) + .subscribe( + { preference -> preference.edit().remove(key).commit() }, + { err -> Timber.d(err, "removeValue error!") } + ) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceHolder.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceHolder.kt new file mode 100644 index 0000000..d0bb804 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferenceHolder.kt @@ -0,0 +1,85 @@ +package guru.core.analytics.data.local + +import android.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import android.util.LruCache +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KClass + +/** + * Created by Haoyi on 2017/12/13. + */ +abstract class PreferenceHolder(val context: Context, + val name: String) { + + val preferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) + + companion object { + @JvmStatic + val EXECUTOR = ThreadPoolExecutor(0, 1, + 60, TimeUnit.SECONDS, LinkedBlockingQueue()) + @JvmStatic + val SCHEDULER = Schedulers.from(EXECUTOR) + @JvmStatic + val CACHE: LruCache = PreferenceCache + } + + protected inline fun bind(key: String, defaultValue: T?): ReadWriteProperty = bind(T::class, key, { defaultValue }) + + protected inline fun bind(key: String, noinline default: () -> T?): ReadWriteProperty = bind(T::class, key, default) + + protected fun bind(clazz: KClass, key: String, default: () -> T?): ReadWriteProperty = PreferenceFieldDelegate(clazz, key, default) + + fun getSharedPreferences(): Single = Single.just(preferences) + + fun getSharedPreferencesDirectly(): SharedPreferences = preferences + + inline fun set(key: String, value: T?) { + if (value != null) CACHE.put(key, value) else CACHE.remove(key) + val clazz = T::class + getSharedPreferences() + .observeOn(SCHEDULER) + .subscribe({ + if (value == null) { + removeValue(key) + } else { + it.edit().apply { putValue(clazz, value, key) }.apply() + } + }, { + it.printStackTrace() + Log.d("Preference", "====> zhy saveNewValue error!", it) + }) + } + + inline fun get(key: String, value: T? = null): T? { + val result = CACHE.get(key) + val clazz = T::class + return try { + when { + clazz.isInstance(result) -> result as T + else -> preferences.getFromPreference(clazz, value, key) + } + } catch (err: Throwable) { + value + } + } + + @SuppressLint("ApplySharedPref") + fun removeValue(key: String) { + CACHE.remove(key) + getSharedPreferences() + .observeOn(SCHEDULER) + .subscribe( + { preference -> preference.edit().remove(key).commit() }, + { err -> Timber.d(err, "removeValue error!") } + ) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt new file mode 100644 index 0000000..e3f7ddd --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PreferencesManager.kt @@ -0,0 +1,51 @@ +package guru.core.analytics.data.local + +import android.annotation.SuppressLint +import android.content.Context + +class PreferencesManager private constructor( + context: Context, +) : PreferenceHolder(context, NAME) { + + companion object { + private const val NAME = "guru_analytics" + const val KEY_TOTAL_DURATION_FG_EVENT = "total_duration_fg_event" + + @SuppressLint("StaticFieldLeak") + @Volatile + private var INSTANCE: PreferencesManager? = null + + fun getInstance(context: Context): PreferencesManager = + INSTANCE ?: synchronized(this) { + INSTANCE ?: PreferencesManager(context.applicationContext).also { INSTANCE = it } + } + } + + fun getTotalDurationFgEvent(): Long { + return try { + getLongDirectly(KEY_TOTAL_DURATION_FG_EVENT, 0L) + } catch (e: Throwable) { + 0L + } + } + + fun setTotalDurationFgEvent(value: Long) { + setLongDirectly(KEY_TOTAL_DURATION_FG_EVENT, value) + } + + private fun getLongDirectly(key: String, defValue: Long = 0L): Long { + return getSharedPreferencesDirectly().getLong(key, defValue) + } + + private fun setLongDirectly(key: String, value: Long) { + getSharedPreferencesDirectly().edit().putLong(key, value).commit() + } + + var isFirstOpen: Boolean? by bind("is_first_open", true) + + var eventCountAll: Int? by bind("event_count_all", 0) + var eventCountDeleted: Int? by bind("event_count_deleted", 0) + var eventCountUploaded: Int? by bind("event_count_uploaded", 0) + var uploadEventBaseUrl: String? by bind("update_event_base_url", "") + var totalDurationFgEvent: Long? by bind("total_duration_fg_event", 0L) +} diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PutValue.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PutValue.kt new file mode 100644 index 0000000..b94de30 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/local/PutValue.kt @@ -0,0 +1,38 @@ +@file:Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY") + +package guru.core.analytics.data.local + +import android.content.SharedPreferences +import kotlin.reflect.KClass + +/** + * Created by Haoyi on 2017/12/13. + */ + +fun SharedPreferences.Editor.putValue(clazz: KClass<*>, value: Any, key: String) { + when (clazz.simpleName) { + "Long" -> putLong(key, value as Long) + "Int" -> putInt(key, value as Int) + "String" -> putString(key, value as String?) + "Boolean" -> putBoolean(key, value as Boolean) + "Float" -> putFloat(key, value as Float) + else -> throw Error("Not found type!") + } +} + +fun SharedPreferences.getFromPreference(clazz: KClass, default: T?, key: String): T = when (clazz.simpleName) { + "Long" -> getLong(key, (default ?: -1L) as Long) as T + "Int" -> getInt(key, (default ?: -1) as Int) as T + "String" -> getString(key, (default ?: "") as String) as T + "Boolean" -> getBoolean(key, (default ?: false) as Boolean) as T + "Float" -> getFloat(key, (default ?: -1.0) as Float) as T + else -> throw Error("Not found type!") +} + +private fun getDefault(clazz: KClass): T? = when(clazz.simpleName) { + "Long" -> -1L + "Int" -> -1 + "Boolean" -> false + "Float" -> -1.0F + else -> null +} as? T \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsInfo.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsInfo.kt new file mode 100644 index 0000000..fdfe00a --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsInfo.kt @@ -0,0 +1,17 @@ +package guru.core.analytics.data.model + +internal data class AnalyticsInfo( + var debug: Boolean = false, + var batchLimit: Int? = null, + var eventExpiredInDays: Int? = 7, + var persistableLog: Boolean = true, + var uploadPeriodInSeconds: Long? = null, + var startUploadDelayInSecond: Long? = 0L, + var eventHandlerCallback: ((Int, String?) -> Unit)? = null, + var isInitPeriodicWork: Boolean = true, + var uploadEventBaseUrl: String? = null, + var fgEventPeriodInSeconds: Long? = null, + var xAppId: String? = null, + var xDeviceInfo: String? = null, + var mainProcess: String? = null, +) diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsOptions.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsOptions.kt new file mode 100644 index 0000000..50002d0 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/AnalyticsOptions.kt @@ -0,0 +1,5 @@ +package guru.core.analytics.data.model + +import guru.core.analytics.data.db.model.EventPriority + +data class AnalyticsOptions(@EventPriority val priority: Int = EventPriority.DEFAULT) \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/Constants.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/Constants.kt new file mode 100644 index 0000000..365e936 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/Constants.kt @@ -0,0 +1,9 @@ +package guru.core.analytics.data.model + +import androidx.annotation.IntDef +import androidx.annotation.StringDef + +object GuruDetails { + val version: Int = 1 +} + diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/EventItem.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/EventItem.kt new file mode 100644 index 0000000..0ed9532 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/EventItem.kt @@ -0,0 +1,13 @@ +package guru.core.analytics.data.model + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class EventItem( + @SerializedName("event_name") val eventName: String, + @SerializedName("item_category") val itemCategory: String? = null, + @SerializedName("item_name") val itemName: String? = null, + @SerializedName("value") val value: Number? = null, + @SerializedName("params") val params: Map? = null, +) \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/exceptions/ArgumentException.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/exceptions/ArgumentException.kt new file mode 100644 index 0000000..dd1f7bc --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/model/exceptions/ArgumentException.kt @@ -0,0 +1,4 @@ +package guru.core.analytics.data.model.exceptions + +class ArgumentException { +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt new file mode 100644 index 0000000..ea3faf0 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/DeviceInfoStore.kt @@ -0,0 +1,43 @@ +package guru.core.analytics.data.store + +import android.content.Context +import android.os.Build +import guru.core.analytics.Constants +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import guru.core.analytics.utils.AndroidUtils +import io.reactivex.subjects.BehaviorSubject +import java.util.* + + +object DeviceInfoStore { + + private val deviceInfoSubject: BehaviorSubject> = + BehaviorSubject.createDefault( + hashMapOf() + ) + + var deviceInfo: Map + get() = deviceInfoSubject.value ?: hashMapOf() + set(value) { + deviceInfoSubject.onNext(value) + } + + fun setDeviceInfo(context: Context) { + val map = hashMapOf() + map[Constants.DeviceInfo.APP_ID] = context.packageName + map[Constants.DeviceInfo.PLATFORM] = Constants.Platform.ANDROID + map[Constants.DeviceInfo.COUNTRY] = Locale.getDefault().country.uppercase() + map[Constants.DeviceInfo.VERSION] = AndroidUtils.getAppVersion(context) + map[Constants.DeviceInfo.TZ_OFFSET] = AndroidUtils.getZoneOffset() + map[Constants.DeviceInfo.DEVICE_TYPE] = AndroidUtils.getDeviceType(context) + map[Constants.DeviceInfo.BRAND] = Build.BRAND + map[Constants.DeviceInfo.MODEL] = Build.MODEL + map[Constants.DeviceInfo.SCREEN_H] = AndroidUtils.getWindowHeight(context) ?: 0 + map[Constants.DeviceInfo.SCREEN_W] = AndroidUtils.getWindowWidth(context) ?: 0 + map[Constants.DeviceInfo.OS_VERSION] = Build.VERSION.RELEASE + map[Constants.DeviceInfo.LANGUAGE] = Locale.getDefault().language + deviceInfoSubject.onNext(map) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.INIT_DEVICE_INFO, map) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/EventInfoStore.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/EventInfoStore.kt new file mode 100644 index 0000000..3c490ef --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/data/store/EventInfoStore.kt @@ -0,0 +1,148 @@ +package guru.core.analytics.data.store + +import guru.core.analytics.Constants +import guru.core.analytics.data.db.model.Event +import guru.core.analytics.data.db.model.EventEntity +import guru.core.analytics.data.db.model.EventPriority +import guru.core.analytics.data.db.model.ParamValue +import guru.core.analytics.data.model.EventItem +import guru.core.analytics.utils.DateTimeUtils +import guru.core.analytics.utils.GsonUtil +import io.reactivex.subjects.BehaviorSubject +import java.util.* + +object EventInfoStore { + + @JvmStatic + val SESSION by lazy { + UUID.randomUUID().toString() + } + + private val propertiesSubject: BehaviorSubject> = + BehaviorSubject.createDefault( + linkedMapOf() + ) + + private val supplementEventParamsSubject: BehaviorSubject> = + BehaviorSubject.createDefault( + hashMapOf(Constants.Event.SCREEN to "main") + ) + + private val idsSubject: BehaviorSubject> = + BehaviorSubject.createDefault( + hashMapOf() + ) + + private var ids: Map + get() = idsSubject.value ?: hashMapOf() + set(value) { + idsSubject.onNext(value) + } + + private var properties: LinkedHashMap + get() = propertiesSubject.value ?: linkedMapOf() + set(value) { + propertiesSubject.onNext(value) + } + + private var supplementEventParams: Map + get() = supplementEventParamsSubject.value ?: hashMapOf(Constants.Event.SCREEN to "main") + set(value) { + supplementEventParamsSubject.onNext(value) + } + + fun setScreen(screen: String) { + supplementEventParams = supplementEventParams.plus(Constants.Event.SCREEN to screen) + } + + fun setUserProperty( + name: String, + value: String? + ) { + properties[name] = value ?: "" + } + + fun removeUserProperties(keys: Set) { + if (keys.isEmpty()) return + keys.forEach { key -> properties.remove(key) } + } + + + private fun setIds(idName: String, id: String) { + ids = ids.plus(idName to id) + } + + fun setDeviceId(deviceId: String) { + setIds(Constants.Ids.DEVICE_ID, deviceId) + } + + fun setUid(uid: String) { + setIds(Constants.Ids.UID, uid) + } + + fun setAdjustId(adjustId: String) { + setIds(Constants.Ids.ADJUST_ID, adjustId) + } + + fun setAdId(adId: String) { + setIds(Constants.Ids.AD_ID, adId) + } + + fun setFirebaseId(firebaseId: String) { + setIds(Constants.Ids.FIREBASE_ID, firebaseId) + } + + private fun createParamValue(value: Any): ParamValue { + return ParamValue().let { + return@let when (value) { + is Int -> ParamValue(i = value.toLong()) + is Long -> ParamValue(i = value) + is Double -> ParamValue(d = value) + is Float -> ParamValue(d = value.toDouble()) + else -> ParamValue(s = value.toString()) + } + } + } + + fun deriveEvent(event: EventItem, priority: Int = EventPriority.DEFAULT, uploading: Boolean = false): EventEntity { + val eventMap = mutableMapOf() + if (!event.itemCategory.isNullOrBlank()) { + eventMap[Constants.Event.ITEM_CATEGORY] = ParamValue(s = event.itemCategory) + } + if (!event.itemName.isNullOrBlank()) { + eventMap[Constants.Event.ITEM_NAME] = ParamValue(s = event.itemName) + } + if (event.value is Number) { + eventMap[Constants.Event.VALUE] = createParamValue(event.value) + } + + event.params?.forEach { entry -> + eventMap[entry.key] = createParamValue(entry.value) + } + + supplementEventParams.forEach { entry -> + eventMap[entry.key] = createParamValue(entry.value) + } + val eventId = UUID.randomUUID().toString() + val eventData = Event( + timestamp = DateTimeUtils.eventAt(), + info = ids, + event = event.eventName, + param = eventMap, + properties = properties, + eventId = eventId, + ) + val eventJson = GsonUtil.gson.toJson(eventData) + return EventEntity( + id = eventId, + session = SESSION, + json = eventJson, + ext = "", + status = if (uploading) 1 else 0, + at = DateTimeUtils.eventAt(), + version = Constants.VERSION, + event = event.eventName, + priority = priority + ) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/ext/JavaLangExt.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/ext/JavaLangExt.kt new file mode 100644 index 0000000..1b4d822 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/ext/JavaLangExt.kt @@ -0,0 +1,44 @@ +package guru.core.analytics.ext + +import timber.log.Timber +import java.io.Closeable +import java.net.URLEncoder +import java.nio.charset.Charset + +/** + * This file contains extension methods for the java.lang package. + */ + +/** + * Helper method to check if a [String] contains another in a case insensitive way. + */ +fun String?.containsCaseInsensitive(other: String?) = + if (this == null && other == null) { + true + } else if (this != null && other != null) { + toLowerCase().contains(other.toLowerCase()) + } else { + false + } + +/** + * Helper extension to URL encode a [String]. Returns an empty string when called on null. + */ +inline val String?.urlEncoded: String + get() = if (Charset.isSupported("UTF-8")) { + URLEncoder.encode(this ?: "", "UTF-8") + } else { + // If UTF-8 is not supported, use the default charset. + @Suppress("deprecation") + URLEncoder.encode(this ?: "") + } + +fun Closeable?.closeQuietly() { + if (this != null) { + try { + this.close() + } catch (e: Exception) { + Timber.e(e) + } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/handler/EventHandler.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/handler/EventHandler.kt new file mode 100644 index 0000000..42a4315 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/handler/EventHandler.kt @@ -0,0 +1,43 @@ +package guru.core.analytics.handler + +import guru.core.analytics.utils.GsonUtil +import java.util.concurrent.CopyOnWriteArraySet + +internal class EventHandler private constructor() { + + companion object { + val INSTANCE by lazy { EventHandler() } + } + + private val listeners = CopyOnWriteArraySet<((Int, String?) -> Unit)>() + + fun hasListener() = listeners.isNotEmpty() + + fun addEventHandler(callback: ((Int, String?) -> Unit)) { + listeners.add(callback) + } + + fun removeEventHandler(callback: ((Int, String?) -> Unit)) { + listeners.remove(callback) + } + + fun clearEventHandler() { + listeners.clear() + } + + fun notifyEventHandler(code: AnalyticsCode, errorInfo: String? = null) { + if (listeners.isEmpty()) return + val iterator = listeners.iterator() + while (iterator.hasNext()) { + val listener = iterator.next() + listener.invoke(code.code, errorInfo) + } + } + + fun notifyEventHandler(code: AnalyticsCode, extMap: Map?) { + if (listeners.isEmpty()) return + val errorInfo = if (extMap.isNullOrEmpty()) null else GsonUtil.gson.toJson(extMap) + notifyEventHandler(code, errorInfo) + } + +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/handler/EventHandlerCode.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/handler/EventHandlerCode.kt new file mode 100644 index 0000000..4b880b9 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/handler/EventHandlerCode.kt @@ -0,0 +1,28 @@ +package guru.core.analytics.handler + +enum class AnalyticsCode(val code: Int) { + STATE_INITIALIZED(1), // SDK 初始化完成 + STATE_START_WORK(2), // 准备完毕, 等待条件满足开始上报 + + INIT_DEVICE_INFO(11), // deviceInfo 设置完成 + DELETE_EXPIRED(12), // 删除过期事件 + UPLOAD_SUCCESS(13), // 上报事件成功 + UPLOAD_FAIL(14), // 上报事件失败 + PERIODIC_WORK_ENQUEUE(15), // 开启PeriodicWork + + NETWORK_AVAILABLE(21), // 网络状态可用 + NETWORK_LOST(22), // 网络状态不可用 + LIFECYCLE_START(23), // app可见 + LIFECYCLE_PAUSE(24), // app不可见 + + ERROR_API(101), // 调用api出错 + ERROR_RESPONSE(102), // api返回结果错误 + ERROR_CACHE_CONTROL(103), // 设置cacheControl出错 + ERROR_DELETE_EXPIRED(104), // 删除过期事件出错 + ERROR_LOAD_MARK(105), // 从数据库取事件以及更改事件状态为正在上报出错 + ERROR_DNS(106), // dns 错误 + ERROR_ZIP(107), // zip 错误 + + EVENT_FIRST_OPEN(1001), // first_open 事件 + EVENT_FG(1002), // fg 事件 +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AnalyticsWorker.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AnalyticsWorker.kt new file mode 100644 index 0000000..9134222 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AnalyticsWorker.kt @@ -0,0 +1,34 @@ +package guru.core.analytics.impl + +import android.content.Context +import androidx.work.RxWorker +import androidx.work.WorkerParameters +import guru.core.analytics.data.db.GuruAnalyticsDatabase +import guru.core.analytics.data.store.DeviceInfoStore +import guru.core.analytics.log.PersistentTree +import io.reactivex.Single +import timber.log.Timber + +class AnalyticsWorker( + val context: Context, + workerParams: WorkerParameters +) : RxWorker(context.applicationContext, workerParams) { + companion object { + const val WORKER_ID = "GuruAnalytics" + const val WORKER_TAG = "Analytics" + } + + override fun createWork(): Single { + Timber.plant(PersistentTree(context)) + GuruAnalyticsDatabase.initialize(context) + DeviceInfoStore.setDeviceInfo(context) + Timber.d("OnWork..") + val engine = EventEngine(context) + return engine.validateEvents().doOnSuccess { + Timber.d("validateEvents deleted:${it.first} reset:${it.second}") + }.toFlowable().flatMap { engine.uploadEvents(500) }.map { true }.toList() + .map { Result.success() } + .onErrorReturn { Result.success() } + } + +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AppLifecycleMonitor.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AppLifecycleMonitor.kt new file mode 100644 index 0000000..dde9dcc --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/AppLifecycleMonitor.kt @@ -0,0 +1,56 @@ +package guru.core.analytics.impl + +import android.content.Context +import android.os.SystemClock +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.ProcessLifecycleOwner +import guru.core.analytics.data.local.PreferencesManager +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import io.reactivex.Flowable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong + + +internal class AppLifecycleMonitor internal constructor(context: Context) { + + private val fgHelper: FgHelper by lazy { + FgHelper(context) + } + + companion object { + private const val TAG = "AppLifecycleMonitor" + } + + + fun initialize() { + ProcessLifecycleOwner.get().lifecycle.addObserver(eventObserver) + fgHelper.ensureInitialized() + } + + private val eventObserver = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_START -> { + Timber.d("${TAG}_ON_START") + fgHelper.start() + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.LIFECYCLE_START) + } + + Lifecycle.Event.ON_RESUME -> Timber.d("${TAG}_ON_RESUME") + Lifecycle.Event.ON_PAUSE -> { + Timber.d("${TAG}_ON_PAUSE") + fgHelper.stop() + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.LIFECYCLE_PAUSE) + + } + + Lifecycle.Event.ON_STOP -> Timber.d("${TAG}_ON_STOP") + else -> {} + } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/ConnectionStateMonitor.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/ConnectionStateMonitor.kt new file mode 100644 index 0000000..2c0b536 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/ConnectionStateMonitor.kt @@ -0,0 +1,61 @@ +package guru.core.analytics.impl + +import android.content.Context +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import guru.core.analytics.utils.AndroidUtils +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable +import io.reactivex.subjects.BehaviorSubject +import timber.log.Timber + +object ConnectionStateMonitor : NetworkCallback() { + + private const val TAG = "ConnectionStateMonitor" + + private var networkRequest: NetworkRequest? = null + private val connectStateSubject: BehaviorSubject = BehaviorSubject.createDefault(false) + + val connectStateFlowable: Flowable + get() = connectStateSubject.toFlowable(BackpressureStrategy.DROP) + + fun bindConnectionStateChanged(context: Context) { + connectStateSubject.onNext(AndroidUtils.isInternetAvailable(context)) + if (networkRequest == null) { + networkRequest = NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build() + } + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + cm?.registerNetworkCallback(networkRequest!!, this) + } + + fun unbindConnectionStateChanged(context: Context?) { + val cm = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + cm?.unregisterNetworkCallback(this) + } + + override fun onAvailable(network: Network) { + super.onAvailable(network) + Timber.d("${TAG}_onAvailable") + if (connectStateSubject.value != true) { + connectStateSubject.onNext(true) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.NETWORK_AVAILABLE) + } + } + + override fun onLost(network: Network) { + super.onLost(network) + Timber.d("${TAG}_onLost") + if (connectStateSubject.value != false) { + connectStateSubject.onNext(false) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.NETWORK_LOST) + } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDeliver.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDeliver.kt new file mode 100644 index 0000000..89a12be --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDeliver.kt @@ -0,0 +1,12 @@ +package guru.core.analytics.impl + +import guru.core.analytics.data.model.AnalyticsOptions +import guru.core.analytics.data.model.EventItem + +sealed interface EventDeliver { + fun deliverEvent(item: EventItem, options: AnalyticsOptions) + + fun deliverProperty(name: String, value: String) + + fun removeProperties(keys: Set) +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDispatcher.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDispatcher.kt new file mode 100644 index 0000000..097d069 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventDispatcher.kt @@ -0,0 +1,54 @@ +package guru.core.analytics.impl + +import guru.core.analytics.data.db.GuruAnalyticsDatabase +import guru.core.analytics.data.model.AnalyticsOptions +import guru.core.analytics.data.model.EventItem +import guru.core.analytics.data.store.EventInfoStore +import timber.log.Timber +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean + +object EventDispatcher : EventDeliver { + + private val pendingEvents = ConcurrentLinkedQueue>() + + private val started = AtomicBoolean(false) + + private fun dispatchPendingEvent() { + Timber.d("EventDispatcher dispatchPendingEvent ${pendingEvents.size}!") + if (pendingEvents.isNotEmpty()) { + while (true) { + val pair = pendingEvents.poll() ?: return + val event = EventInfoStore.deriveEvent(pair.first, priority = pair.second.priority) + GuruAnalyticsDatabase.getInstance().eventDao().addEvent(event) + } + } + } + + fun start() { + if (started.compareAndSet(false, true)) { + Timber.d("EventDispatcher started!") + dispatchPendingEvent() + } + } + + override fun deliverEvent(item: EventItem, options: AnalyticsOptions) { + if (started.get()) { + Timber.d("EventDispatcher deliverEvent!") + dispatchPendingEvent() + val event = EventInfoStore.deriveEvent(item, priority = options.priority) + GuruAnalyticsDatabase.getInstance().eventDao().addEvent(event) + } else { + Timber.d("EventDispatcher deliverEvent pending!") + pendingEvents.offer(item to options) + } + } + + override fun deliverProperty(name: String, value: String) { + EventInfoStore.setUserProperty(name, value) + } + + override fun removeProperties(keys: Set) { + EventInfoStore.removeUserProperties(keys) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventEngine.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventEngine.kt new file mode 100644 index 0000000..b722c35 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/EventEngine.kt @@ -0,0 +1,312 @@ +package guru.core.analytics.impl + +import android.content.Context +import guru.core.analytics.Constants +import guru.core.analytics.GuruAnalytics +import guru.core.analytics.data.api.GuruRepository +import guru.core.analytics.data.api.ServiceLocator +import guru.core.analytics.data.db.GuruAnalyticsDatabase +import guru.core.analytics.data.db.model.EventEntity +import guru.core.analytics.data.db.model.EventPriority +import guru.core.analytics.data.db.model.EventStatistic +import guru.core.analytics.data.local.PreferencesManager +import guru.core.analytics.data.model.AnalyticsOptions +import guru.core.analytics.data.model.EventItem +import guru.core.analytics.data.store.EventInfoStore +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import guru.core.analytics.log.PersistentTree +import guru.core.analytics.utils.AndroidUtils +import guru.core.analytics.utils.ApiParamUtils +import guru.core.analytics.utils.DateTimeUtils +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.collections.ArrayList +import kotlin.math.ceil + +internal class EventEngine internal constructor( + private val context: Context, + private val batchLimit: Int = DEFAULT_BATCH_LIMIT, + private val uploadPeriodInSeconds: Long = DEFAULT_UPLOAD_PERIOD_IN_SECONDS, + private val eventExpiredInDays: Int = DEFAULT_EVENT_EXPIRED_IN_DAYS, + private val uploadEventBaseUrl: String? = null, +) : EventDeliver { + private val guruRepository: GuruRepository by lazy { + ServiceLocator.provideGuruRepository(context.applicationContext, baseUrl = uploadEventBaseUrl) + } + + private val preferencesManager by lazy { + PreferencesManager.getInstance(context) + } + + private val lifecycleMonitor = AppLifecycleMonitor(context) + + companion object { + private const val TAG = "UploadEvents" + const val DEFAULT_UPLOAD_PERIOD_IN_SECONDS = 60L // 接口上传间隔时间 秒 + const val DEFAULT_BATCH_LIMIT = 25 // 一次上传event最多数量 + const val DEFAULT_EVENT_EXPIRED_IN_DAYS = 7 + + + private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor()) + private val dbScheduler = Schedulers.from(Executors.newSingleThreadExecutor()) + + private val dateTimeFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + + + fun logDebug(message: String, vararg args: Any?) { + if (GuruAnalytics.INSTANCE.isDebug()) { + Timber.tag(TAG).d(message, args) + } + } + + fun logEvent(entity: EventEntity) { + Timber.log( + PersistentTree.PRIORITY_EVENT, + "[${dateTimeFormatter.format(entity.at)}] ${entity.event} ${entity.json}" + ) + } + } + + private var compositeDisposable: CompositeDisposable = CompositeDisposable() + private val pendingEventSubject: PublishSubject = PublishSubject.create() + private val forceUploadSubject: PublishSubject = PublishSubject.create() + + private val started = AtomicBoolean(false) + + fun start(startUploadDelay: Long?) { + if (started.compareAndSet(false, true)) { + prepare() + ConnectionStateMonitor.bindConnectionStateChanged(context.applicationContext) + lifecycleMonitor.initialize() + scheduler.scheduleDirect({ + EventDispatcher.start() + logFirstOpen() + startWork() + val extMap = mapOf("startUploadDelayInSecond" to startUploadDelay) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.STATE_START_WORK, extMap) + }, startUploadDelay ?: 0, TimeUnit.SECONDS) + } + } + + private fun logFirstOpen() { + if (preferencesManager.isFirstOpen == true) { + GuruAnalytics.INSTANCE.logEvent( + Constants.Event.FIRST_OPEN, + options = AnalyticsOptions(EventPriority.EMERGENCE) + ) + preferencesManager.isFirstOpen = false + + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FIRST_OPEN) + } + } + + private fun startWork() { + pollEvents() + forceUpload() + Timber.d("UploadEventDaemon started!!") + } + + private fun splitEntities(entities: List): Flowable> { + val length = entities.size + val num = ceil(length.toDouble() / batchLimit).toInt() + val newList: MutableList> = ArrayList(num) + for (i in 0 until num) { + val fromIndex = i * batchLimit + val toIndex = length.coerceAtMost((i + 1) * batchLimit) + newList.add(entities.subList(fromIndex, toIndex)) + } + return Flowable.fromIterable(newList) + } + + private fun prepare() { + compositeDisposable.add( + validateEvents().subscribe({ + Timber.d("validateEvents deleted:${it.first} reset:${it.second}") + }, Timber::e) + ) + } + + private fun pollEvents() { + logDebug("pollEvents()!! $uploadPeriodInSeconds $batchLimit") + val flowable = + pendingEventSubject.buffer(uploadPeriodInSeconds, TimeUnit.SECONDS, batchLimit) + .toFlowable(BackpressureStrategy.DROP) + .doOnNext { + logDebug("pendingEvent ${it.size}") + } + val networkFlowable = ConnectionStateMonitor.connectStateFlowable.doOnNext { + logDebug("network $it") + } + val forceFlowable = forceUploadSubject.toFlowable(BackpressureStrategy.DROP) + + compositeDisposable.add(Flowable.combineLatest( + Flowable.merge(flowable, forceFlowable), networkFlowable + ) { _, connected -> connected } + .filter { + logDebug("pollEvent filter $it") + return@filter it + } + .flatMap { uploadEvents(500) } + .subscribe() + ) + } + + internal fun validateEvents(): Single> { + return Single.create> { emitter -> + // 删除过期数据 + val currentTs = DateTimeUtils.eventAt() + val expiredTs = currentTs - eventExpiredInDays * DateTimeUtils.DAYS_IN_MILLIS + + Timber.d("validateEvents $currentTs $expiredTs $eventExpiredInDays") + val pair = GuruAnalyticsDatabase.getInstance().eventDao() + .deleteExpiredEvents(expiredTs) + // 记录删除的事件数量 + if (pair.first > 0) { + val eventCountDeleted = preferencesManager.eventCountDeleted ?: 0 + preferencesManager.eventCountDeleted = eventCountDeleted + pair.first + + val extMap = mapOf( + "expiredCount" to pair.first, + "allDeleteCount" to preferencesManager.eventCountDeleted + ) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.DELETE_EXPIRED, extMap) + } + emitter.onSuccess(pair) + }.subscribeOn(dbScheduler) + .onErrorReturn { + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_DELETE_EXPIRED, it.message) + Pair(-1, -1) + } + } + + internal fun uploadEvents(count: Int): Flowable> { + val eventDao = GuruAnalyticsDatabase.getInstance().eventDao() + logDebug("uploadEvents: $count") + return Flowable.just(count).map { eventDao.loadAndMarkUploadEvents(it) } + .onErrorReturn { + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_LOAD_MARK, it.message) + eventDao.loadAndMarkUploadEvents(batchLimit) + } + .filter { it.isNotEmpty() } + .subscribeOn(dbScheduler) + .observeOn(scheduler) + .concatMap { splitEntities(it) } + .flatMapSingle { uploadEventsInternal(it) } + .filter { it.isNotEmpty() } + .doOnNext { + eventDao.deleteEvents(it) + if (GuruAnalytics.INSTANCE.isDebug()) { + for (entity in it) { + logEvent(entity) + } + } + } + } + + private fun uploadEventsInternal( + entities: List, + withoutDelay: Boolean = false + ): Single> { + val param = ApiParamUtils.generateApiParam(entities) + return guruRepository.uploadEvents(param).map { true } + .observeOn(dbScheduler) + .doOnError { + if (withoutDelay) { + GuruAnalyticsDatabase.getInstance().eventDao().addEvents(entities) + } else { + GuruAnalyticsDatabase.getInstance().eventDao().updateEventDefault(entities) + } + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_FAIL, it.message) + } + .doOnSuccess { + // 记录上传成功的数量 + val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0 + preferencesManager.eventCountUploaded = eventCountUploaded + entities.size + + val extMap = mapOf( + "count" to entities.size, + "eventNames" to entities.joinToString(",") { it.event }, + "allUploadedCount" to preferencesManager.eventCountUploaded, + ) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.UPLOAD_SUCCESS, extMap) + } + .onErrorReturn { false } + .map { if (it) entities else emptyList() } + } + + + private fun logEventWithoutDelay( + eventName: String, + itemCategory: String? = null, + itemName: String? = null, + value: Int? = null, + parameters: Map? = null + ) { + val item = EventItem( + eventName = eventName, + itemCategory = itemCategory, + itemName = itemName, + value = value, + params = parameters + ) + val event = EventInfoStore.deriveEvent(item) + compositeDisposable.add( + Flowable.just(mutableListOf(event)) + .flatMap { + return@flatMap if (!AndroidUtils.isInternetAvailable(context) + ) { + GuruAnalyticsDatabase.getInstance().eventDao().addEvents(it) + Flowable.empty() + } else { + Flowable.just(it) + } + } + .flatMapSingle { uploadEventsInternal(it, withoutDelay = true) } + .subscribe {} + ) + } + + override fun deliverEvent(item: EventItem, options: AnalyticsOptions) { + pendingEventSubject.onNext(1) + // 记录收到的事件数量 + increaseEventCount() + if (options.priority == EventPriority.EMERGENCE) { + forceUpload() + } + } + + private fun increaseEventCount() { + val eventCountAll = preferencesManager.eventCountAll ?: 0 + preferencesManager.eventCountAll = eventCountAll + 1 + } + + override fun deliverProperty(name: String, value: String) { + + } + + override fun removeProperties(keys: Set) { + + } + + private fun forceUpload() { + forceUploadSubject.onNext(true) + } + + fun getEventsStatics(): EventStatistic { + val eventCountAll = preferencesManager.eventCountAll ?: 0 + val eventCountDeleted = preferencesManager.eventCountDeleted ?: 0 + val eventCountUploaded = preferencesManager.eventCountUploaded ?: 0 + return EventStatistic(eventCountAll, eventCountDeleted, eventCountUploaded) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgEventHelper.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgEventHelper.kt new file mode 100644 index 0000000..f1ded3a --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgEventHelper.kt @@ -0,0 +1,136 @@ +//package guru.core.analytics.impl +// +//import android.content.Context +//import android.os.SystemClock +//import guru.core.analytics.Constants +//import guru.core.analytics.GuruAnalytics +//import guru.core.analytics.data.db.model.EventEntity +//import guru.core.analytics.data.db.model.EventPriority +//import guru.core.analytics.data.local.PreferencesManager +//import guru.core.analytics.data.model.EventItem +//import guru.core.analytics.data.store.EventInfoStore +//import guru.core.analytics.handler.AnalyticsCode +//import guru.core.analytics.handler.EventHandler +//import io.reactivex.BackpressureStrategy +//import io.reactivex.Flowable +//import io.reactivex.disposables.CompositeDisposable +//import io.reactivex.schedulers.Schedulers +//import io.reactivex.subjects.BehaviorSubject +//import timber.log.Timber +//import java.util.concurrent.Executors +//import java.util.concurrent.TimeUnit +//import java.util.concurrent.atomic.AtomicLong +// +//class FgEventHelper private constructor() { +// +// companion object { +// private const val TAG = "FgEventHelper" +// private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor()) +// private const val DEFAULT_FG_UPLOAD_INTERVAL_SECOND = 30L +// private const val MINIMUM_FG_UPLOAD_INTERVAL_SECOND = 15L +// private const val MAXIMUM_FG_UPLOAD_INTERVAL_SECOND = 60 * 60 * 1000L +// +// @Volatile +// private var INSTANCE: FgEventHelper? = null +// +// fun getInstance(): FgEventHelper = INSTANCE ?: synchronized(this) { +// INSTANCE ?: FgEventHelper().also { INSTANCE = it } +// } +// } +// +// private var compositeDisposable: CompositeDisposable = CompositeDisposable() +// private val lifecycleSubject: BehaviorSubject = BehaviorSubject.create() +// private val lastTime = AtomicLong(0L) +// private var fgDuration = AtomicLong(0L) +// private fun updateFgDuration(duration: Long) { +// fgDuration.set(duration) +// preferencesManager?.setTotalDurationFgEvent(duration) +// } +// +// private var preferencesManager: PreferencesManager? = null +// +// +// fun start(context: Context, fgEventPeriodInSeconds: Long? = null) { +// preferencesManager = PreferencesManager.getInstance(context) +// AppLifecycleMonitor.getInstance().addLifecycleMonitor { +// lifecycleSubject.onNext(it) +// Timber.tag(TAG).d("start addLifecycleMonitor isVisible:$it") +// } +// +// val periodInSeconds = fgEventPeriodInSeconds ?: -1L +// val interval = +// if (periodInSeconds < 0) DEFAULT_FG_UPLOAD_INTERVAL_SECOND else if (periodInSeconds < MINIMUM_FG_UPLOAD_INTERVAL_SECOND) MINIMUM_FG_UPLOAD_INTERVAL_SECOND else periodInSeconds +// +// val fgIntervalFlow = Flowable.interval(0, interval, TimeUnit.SECONDS) +// compositeDisposable.add( +// Flowable.combineLatest( +// fgIntervalFlow, lifecycleSubject.toFlowable(BackpressureStrategy.DROP) +// ) { _, isVisible -> isVisible } +// .doOnSubscribe { logFirstFgEvent() } +// .subscribeOn(scheduler) +// .observeOn(scheduler) +// .map { getFgDuration(MAXIMUM_FG_UPLOAD_INTERVAL_SECOND) } +// .filter { it > 0L } +// .subscribe({ duration -> logFgEvent(duration) }, Timber::e) +// ) +// Timber.tag(TAG).d("start interval:$interval ${compositeDisposable.size()}") +// } +// +// +// private fun logFirstFgEvent() { +// if (preferencesManager == null) return +// val cachedDuration = 1L.coerceAtLeast(preferencesManager!!.getTotalDurationFgEvent()) +// logFgEvent(cachedDuration) +// updateFgDuration(0L) +// Timber.tag(TAG).d("logFirstFgEvent $cachedDuration") +// } +// +// private fun logFgEvent(duration: Long) { +// if (duration <= 0L) return +// val params = mutableMapOf() +// params[Constants.Event.DURATION] = duration +// GuruAnalytics.INSTANCE.logEvent(Constants.Event.FG, parameters = params) +// val extMap = mapOf("duration" to duration) +// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FG, extMap) +// } +// +// fun getFgEvent(limit: Long = 0L): EventEntity? { +// val duration = getFgDuration(limit) +// return if (duration > 0L) { +// val extMap = mapOf("duration" to duration) +// EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FG, extMap) +// EventInfoStore.deriveEvent( +// EventItem( +// Constants.Event.FG, params = mapOf(Pair(Constants.Event.DURATION, duration)) +// ), +// priority = EventPriority.EMERGENCE, +// ) +// } else { +// null +// } +// } +// +// @Synchronized +// fun getFgDuration(limit: Long = 0L): Long { +// val lastTimeMillions = lastTime.get() +// val currentTimeMillions = SystemClock.elapsedRealtime() +// val isVisible = lifecycleSubject.value ?: false +// lastTime.set(if (isVisible) currentTimeMillions else 0L) +// val totalDuration = if (lastTimeMillions > 0L) { +// fgDuration.get() + currentTimeMillions - lastTimeMillions +// } else { +// fgDuration.get() +// } +// Timber.tag(TAG).d("getFgDuration limit:$limit isVisible:$isVisible totalDuration:$totalDuration") +// return if (totalDuration > 0L.coerceAtLeast(limit)) { +// updateFgDuration(0L) +// totalDuration +// } else { +// if (lastTimeMillions > 0) { +// updateFgDuration(totalDuration) +// } +// 0L +// } +// } +// +//} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgHelper.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgHelper.kt new file mode 100644 index 0000000..c0adef1 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/FgHelper.kt @@ -0,0 +1,93 @@ +package guru.core.analytics.impl + +import android.content.Context +import android.os.SystemClock +import guru.core.analytics.Constants +import guru.core.analytics.GuruAnalytics +import guru.core.analytics.data.local.PreferencesManager +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import io.reactivex.Flowable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.lang.Math.max +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +internal class FgHelper(context: Context) { + + companion object { + private const val FG_RECORD_INTERVAL_SECOND = 5L + private const val FG_REPORT_INTERVAL_MILLIS = 40 * 1000L + private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor()) + } + + @Volatile + private var latestRecordAt = SystemClock.elapsedRealtime() + + @Volatile + private var currentDuration = 0L + private var disposable: Disposable? = null + private val initialized = AtomicBoolean(false) + + private val preferencesManager: PreferencesManager by lazy { + PreferencesManager.getInstance(context) + } + + private val preferenceEditor by lazy { + preferencesManager.getSharedPreferencesDirectly().edit() + } + + internal fun ensureInitialized() { + if (initialized.compareAndSet(false, true)) { + currentDuration = preferencesManager.getTotalDurationFgEvent() + refresh(force = true) + } + } + + private fun refresh(force: Boolean = false): Long { + val now = SystemClock.elapsedRealtime() + val duration = now - latestRecordAt + val fgDuration = currentDuration + duration + latestRecordAt = now + currentDuration = if (force || fgDuration > FG_REPORT_INTERVAL_MILLIS) { + logFgEvent(fgDuration.coerceAtLeast(1)) + 0L + } else { + fgDuration + } + + preferenceEditor.putLong(PreferencesManager.KEY_TOTAL_DURATION_FG_EVENT, currentDuration) + .commit() + return currentDuration + } + + fun start() { + ensureInitialized() + latestRecordAt = SystemClock.elapsedRealtime() + disposable?.dispose() + disposable = Flowable.interval(0, FG_RECORD_INTERVAL_SECOND, TimeUnit.SECONDS) + .subscribeOn(scheduler) + .subscribe({ + refresh() + }, { + Timber.tag("FgHelper").e(it) + }) + } + + fun stop() { + disposable?.dispose() + refresh() + } + + private fun logFgEvent(duration: Long) { + if (duration <= 0L) return + val params = mutableMapOf() + params[Constants.Event.DURATION] = duration + GuruAnalytics.INSTANCE.logEvent(Constants.Event.FG, parameters = params) + val extMap = mapOf("duration" to duration) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.EVENT_FG, extMap) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/GuruAnalyticsImpl.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/GuruAnalyticsImpl.kt new file mode 100644 index 0000000..d6cffaa --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/impl/GuruAnalyticsImpl.kt @@ -0,0 +1,295 @@ +package guru.core.analytics.impl + +import android.content.Context +import androidx.annotation.RequiresPermission +import androidx.work.* +import guru.core.analytics.Constants +import guru.core.analytics.GuruAnalytics +import guru.core.analytics.data.api.ServiceLocator +import guru.core.analytics.data.db.GuruAnalyticsDatabase +import guru.core.analytics.data.db.model.EventStatistic +import guru.core.analytics.data.local.PreferencesManager +import guru.core.analytics.data.model.AnalyticsOptions +import guru.core.analytics.data.model.EventItem +import guru.core.analytics.data.store.DeviceInfoStore +import guru.core.analytics.data.store.EventInfoStore +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import guru.core.analytics.log.PersistentTree +import guru.core.analytics.utils.AndroidUtils +import guru.core.analytics.utils.EventChecker +import guru.core.analytics.utils.SystemProperties +import timber.log.Timber +import java.io.File +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +internal class GuruAnalyticsImpl : GuruAnalytics() { + + companion object { + private val reservedEventNames = setOf("test") + private val internalVersion = "0.2.1.0" + } + + private val initialized = AtomicBoolean(false) + + private var engine: EventEngine? = null + + private var lifecycleMonitor: AppLifecycleMonitor? = null + + private var debugMode = false + + private val deliverExecutor = Executors.newSingleThreadExecutor() + + private val delivers = CopyOnWriteArrayList() + + override fun isDebug(): Boolean = debugMode + + override fun setDebug(debug: Boolean) { + debugMode = debug + } + + override fun setUploadEventBaseUrl(context: Context, updateEventBaseUrl: String) { + if (updateEventBaseUrl.isEmpty()) { + throw IllegalArgumentException("updateEventBaseUrl:${updateEventBaseUrl} is empty") + } + ServiceLocator.updateAnalyticsBaseUrl(context.applicationContext, updateEventBaseUrl) + PreferencesManager.getInstance(context).uploadEventBaseUrl = updateEventBaseUrl + Timber.d("setUploadEventBaseUrl:$updateEventBaseUrl") + } + + @RequiresPermission(allOf = ["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]) + override fun initialize( + context: Context, + batchLimit: Int?, + uploadPeriodInSeconds: Long?, + startUploadDelayInSecond: Long?, + eventExpiredInDays: Int?, + debug: Boolean, + persistableLog: Boolean, + eventHandlerCallback: ((Int, String?) -> Unit)?, + isInitPeriodicWork: Boolean, + uploadEventBaseUrl: String?, + fgEventPeriodInSeconds: Long?, + xAppId: String?, + xDeviceInfo: String?, + mainProcess: String? + ) { + if (initialized.compareAndSet(false, true)) { + delivers.add(EventDispatcher) + eventHandlerCallback?.let { EventHandler.INSTANCE.addEventHandler(it) } + val debugApp = SystemProperties.read("debug.guru.analytics.app") + val forceDebug = debugApp == context.packageName + if (forceDebug || persistableLog) { + Timber.plant(PersistentTree(context, debug = debug)) + } + debugMode = debug + Timber.d("[$internalVersion]initialize batchLimit:$batchLimit uploadPeriodInSecond:$uploadPeriodInSeconds startUploadDelayInSecond:$startUploadDelayInSecond eventExpiredInDays:$eventExpiredInDays debug:$debug") + GuruAnalyticsDatabase.initialize(context.applicationContext) + + DeviceInfoStore.setDeviceInfo(context) + ServiceLocator.setDebug(debug) + ServiceLocator.addHeaderParam("X-APP-ID", xAppId) + ServiceLocator.addHeaderParam("X-DEVICE-INFO", xDeviceInfo) + + engine = EventEngine( + context, + batchLimit = batchLimit ?: EventEngine.DEFAULT_BATCH_LIMIT, + uploadPeriodInSeconds = uploadPeriodInSeconds + ?: EventEngine.DEFAULT_UPLOAD_PERIOD_IN_SECONDS, + eventExpiredInDays = EventEngine.DEFAULT_EVENT_EXPIRED_IN_DAYS.coerceAtLeast( + eventExpiredInDays ?: EventEngine.DEFAULT_EVENT_EXPIRED_IN_DAYS + ), + uploadEventBaseUrl = uploadEventBaseUrl, + ).apply { + start(startUploadDelayInSecond) + delivers.add(this) + } + if (isInitPeriodicWork) { + val process = AndroidUtils.getProcessName(context) + Timber.d("initialize ${mainProcess == process} currentProcess:$process mainProcess:$mainProcess") + if (mainProcess.isNullOrBlank() || mainProcess == process) { + initAnalyticsPeriodic(context) + } else { + if (!process.isNullOrBlank()) { + val params = mutableMapOf() + params[Constants.Event.PROCESS] = process + INSTANCE.logEvent(Constants.Event.ERROR_PROCESS, parameters = params) + } + } + } + + val extMap = mapOf( + "version_code" to internalVersion, + "batchLimit" to batchLimit, + "uploadPeriodInSecond" to uploadPeriodInSeconds, + "startUploadDelayInSecond" to startUploadDelayInSecond, + "eventExpiredInDays" to eventExpiredInDays, + "debug" to debug, + ) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.STATE_INITIALIZED, extMap) + if (!uploadEventBaseUrl.isNullOrEmpty()) { + PreferencesManager.getInstance(context).uploadEventBaseUrl = uploadEventBaseUrl + } + } + } + + private fun initAnalyticsPeriodic(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = PeriodicWorkRequestBuilder( + 6, TimeUnit.HOURS, + 15, TimeUnit.MINUTES + ) + .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.MINUTES) + .addTag(AnalyticsWorker.WORKER_TAG) + .setConstraints(constraints) + .build() + Timber.d("initAnalyticsPeriodic") + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork( + AnalyticsWorker.WORKER_ID, + ExistingPeriodicWorkPolicy.REPLACE, request + ) + + val extMap = mapOf( + "repeatInterval" to "6h", + "flexTimeInterval" to "15m", + ) + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.PERIODIC_WORK_ENQUEUE, extMap) + } + + override fun setDeviceId(deviceId: String) { + EventInfoStore.setDeviceId(deviceId) + Timber.d("setDeviceId:$deviceId") + } + + override fun setUid(uid: String) { + EventInfoStore.setUid(uid) + Timber.d("setUid:$uid") + } + + override fun setAdjustId(adjustId: String) { + EventInfoStore.setAdjustId(adjustId) + Timber.d("setAdjustId:$adjustId") + } + + override fun setAdId(adId: String) { + EventInfoStore.setAdId(adId) + Timber.d("setAdId:$adId") + } + + override fun setFirebaseId(firebaseId: String) { + EventInfoStore.setFirebaseId(firebaseId) + Timber.d("setFirebaseId:$firebaseId") + } + + override fun setScreen(screenName: String) { + EventInfoStore.setScreen(screenName) + Timber.d("setScreen:$screenName") + } + + override fun zipLogs(context: Context): File? { + return kotlin.runCatching { PersistentTree(context).zipLogs() }.getOrNull() + } + + override fun logEvent( + eventName: String, + itemCategory: String?, + itemName: String?, + value: Number?, + parameters: Map?, + options: AnalyticsOptions + ) { + if (eventName.isBlank() || + eventName.length > 128 || + !EventChecker.isAlphabet(eventName[0]) || + EventChecker.containsNonAlphaNumeric(eventName) + ) { + throw IllegalArgumentException( + "name${eventName} must contain 1 to 128 alphanumeric characters." + ) + } + if (reservedEventNames.contains(eventName)) { + throw IllegalArgumentException( + "Event name($eventName) is reserved and cannot be used", + ) + } + + val event = EventItem( + eventName = eventName, + itemCategory = itemCategory, + itemName = itemName, + value = value, + params = parameters + ) + + deliverEvent(event, options) + } + + override fun setUserProperty(key: String, value: String) { + if (key.isBlank() || + key.length > 24 || + !EventChecker.isAlphabet(key[0]) || + EventChecker.containsNonAlphaNumeric(key) + ) { + throw IllegalArgumentException( + "name${key} must contain 1 to 24 alphanumeric characters." + ) + } + + deliverProperty(key, value) + } + + override fun getEventsStatics(): EventStatistic { + return engine?.getEventsStatics() ?: EventStatistic() + } + + override fun addEventHandler(listener: (Int, String?) -> Unit) { + EventHandler.INSTANCE.addEventHandler(listener) + } + + override fun removeEventHandler(listener: (Int, String?) -> Unit) { + EventHandler.INSTANCE.removeEventHandler(listener) + } + + override fun removeUserProperty(key: String) { + removeProperties(setOf(key)) + } + + override fun removeUserProperties(keys: Set) { + removeProperties(keys) + } + + private fun deliverEvent(item: EventItem, options: AnalyticsOptions) { + Timber.tag("GuruAnalytics").d("deliverEvent ${item.eventName}!") + deliverExecutor.execute { + for (deliver in delivers) { + deliver.deliverEvent(item, options) + } + } + } + + private fun deliverProperty(name: String, value: String) { + Timber.tag("GuruAnalytics").d("deliverProperty $name = $value") + deliverExecutor.execute { + for (deliver in delivers) { + deliver.deliverProperty(name, value) + } + } + } + + private fun removeProperties(keys: Set) { + Timber.tag("GuruAnalytics").d("removeProperties $keys") + deliverExecutor.execute { + for (deliver in delivers) { + deliver.removeProperties(keys) + } + } + } + +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/log/LogFormatter.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/log/LogFormatter.kt new file mode 100644 index 0000000..acdf60e --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/log/LogFormatter.kt @@ -0,0 +1,29 @@ +package guru.core.analytics.log + +import java.text.SimpleDateFormat +import java.util.* +import java.util.logging.Formatter +import java.util.logging.LogRecord + +class MainFormatter : Formatter() { + + private val dateTimeFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + + override fun format(record: LogRecord?): String { + if (record == null) return "" + val formatTime = dateTimeFormatter.format(record.millis) + val message = formatMessage(record) + return "$formatTime ${record.level.name} $message\n" + } +} + + +class EventFormatter : Formatter() { + + + override fun format(record: LogRecord?): String { + if (record == null) return "" + val message = formatMessage(record) + return "$message\n" + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/log/PersistentTree.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/log/PersistentTree.kt new file mode 100644 index 0000000..733c114 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/log/PersistentTree.kt @@ -0,0 +1,101 @@ +package guru.core.analytics.log + +import android.content.Context +import android.os.Environment +import android.util.Log +import guru.core.analytics.utils.ZipUtils +import timber.log.Timber +import java.io.File +import java.util.logging.FileHandler +import java.util.logging.Level +import java.util.logging.Logger + +internal class PersistentTree(context: Context, private val debug: Boolean = false) : Timber.Tree() { + + companion object { + // 日志所在目录名称 + private const val LOGS_DIRECTORY = "guru_analytics/logs" + + // 日志文件名称 + private const val LOG_FILE_NAME_EVENT = "event.dat" + private const val LOG_FILE_NAME_MAIN = "main.log" + + private const val LOGGER_NAME_EVENT = "event" + private const val LOGGER_NAME_MAIN = "main" + + // 单一日志文件大小限制 + private const val FILE_SIZE_IN_BYTE = 10 * 1024 * 1024 + + // 日志文件最大数量 + private const val FILE_COUNT_EVENT = 5 + private const val FILE_COUNT_DAILY = 7 + + const val PRIORITY_EVENT = 10000 + } + + private val eventLevel = object : Level("EVENT", PRIORITY_EVENT, "guru.analytics.event") {} + + private val resolvedLogsDirectory by lazy { + File( + if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() && !Environment.isExternalStorageRemovable()) { + context.getExternalFilesDir(null) + } else { + context.filesDir + }, LOGS_DIRECTORY + ).also { it.mkdirs() } + } + + private val mainLogger by lazy { + Logger.getLogger(LOGGER_NAME_MAIN).also { + val analyticsFileName = resolvedLogsDirectory.path + "/" + LOG_FILE_NAME_MAIN + kotlin.runCatching { + FileHandler(analyticsFileName, FILE_SIZE_IN_BYTE, FILE_COUNT_DAILY, true) + }.getOrNull()?.let { fileHandler -> + fileHandler.formatter = MainFormatter() + it.useParentHandlers = debug + it.addHandler(fileHandler) + } + } + } + + private val eventLogger by lazy { + Logger.getLogger(LOGGER_NAME_EVENT).also { + val analyticsFileName = resolvedLogsDirectory.path + "/" + LOG_FILE_NAME_EVENT + kotlin.runCatching { + FileHandler(analyticsFileName, FILE_SIZE_IN_BYTE, FILE_COUNT_EVENT, true) + }.getOrNull()?.let { fileHandler -> + fileHandler.formatter = EventFormatter() + it.useParentHandlers = debug + it.addHandler(fileHandler) + } + } + } + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + if (priority == eventLevel.intValue()) { + eventLogger.log(eventLevel, message) + } else { + val level = if (priority == Log.WARN) { + Level.WARNING + } else if (priority >= Log.ERROR) { + Level.SEVERE + } else { + Level.INFO + } + val maybeTag = if (tag != null) "[$tag] " else "" + mainLogger.log(level, "$maybeTag${message.trim()}") + } + } + + fun zipLogs(): File? { + if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED) { + Timber.e("SD card not mounted.") + return null + } + + val destFile = File(resolvedLogsDirectory.path + ".zip").also { it.deleteOnExit() } + val ret = ZipUtils.zip(resolvedLogsDirectory.absolutePath, destFile.absolutePath) + + return if (ret) destFile else null + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/AndroidUtils.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/AndroidUtils.kt new file mode 100644 index 0000000..456bd92 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/AndroidUtils.kt @@ -0,0 +1,143 @@ +package guru.core.analytics.utils + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Point +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import android.os.Process +import android.os.StrictMode +import android.os.StrictMode.VmPolicy +import android.text.TextUtils +import android.util.TypedValue +import android.view.WindowManager +import androidx.annotation.Dimension +import guru.core.analytics.BuildConfig +import guru.core.analytics.Constants +import java.io.BufferedReader +import java.io.File +import java.io.FileInputStream +import java.io.InputStreamReader +import java.util.* + + +object AndroidUtils { + + fun isInternetAvailable(context: Context?): Boolean { + val cm = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val networkCapabilities = cm?.activeNetwork ?: return false + val actNw = cm.getNetworkCapabilities(networkCapabilities) ?: return false + when { + actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true // wifi + actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true // 蜂窝网 + actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true // 以太网 + else -> false + } + } else { + cm?.activeNetworkInfo?.isConnected ?: false + } + } + + fun getDeviceType(context: Context?): String { + if (context == null) return Constants.DeviceType.MOBILE + // 同AppLovinSdkUtils.isTablet() 保持一致 + val point = getPoint(context) + val minSize: Int = point.x.coerceAtMost(point.y) + val isTablet = minSize >= dpToPx(context, 600) + +// // 方法一 +// val isTablet1 = ((context.resources.configuration.screenLayout ?: 0) +// and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE +// +// // 方法二, 根据是否可以打电话 +// val manager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager +// val isTablet2 = manager?.phoneType == TelephonyManager.PHONE_TYPE_NONE + + return if (isTablet) Constants.DeviceType.TABLET else Constants.DeviceType.MOBILE + } + + private fun getPoint(context: Context?): Point { + val point = Point().apply { + x = 480 + y = 320 + } + val vmPolicy = StrictMode.getVmPolicy() + StrictMode.setVmPolicy(VmPolicy.LAX) + val wm = context?.getSystemService("window") as? WindowManager + wm?.defaultDisplay?.getRealSize(point) + StrictMode.setVmPolicy(vmPolicy) + return point + } + + fun dpToPx(c: Context, @Dimension(unit = 0) dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + c.resources.displayMetrics + ).toInt() + } + + fun getZoneOffset() = Calendar.getInstance().get(Calendar.ZONE_OFFSET) + + fun getAppVersion(context: Context?): String { + try { + return context?.let { + it.packageManager.getPackageInfo(it.packageName, 0).versionName + } ?: "" + } catch (e: PackageManager.NameNotFoundException) { + } + return "" + } + + fun getWindowWidth(context: Context?) = context?.resources?.displayMetrics?.widthPixels + fun getWindowHeight(context: Context?) = context?.resources?.displayMetrics?.heightPixels + + fun getProcessName(context: Context): String? { + val cmdFile = File("/proc/self/cmdline") + if (cmdFile.exists() && !cmdFile.isDirectory) { + var reader: BufferedReader? = null + try { + reader = BufferedReader(InputStreamReader(FileInputStream(cmdFile))) + val procName = reader.readLine() + if (!TextUtils.isEmpty(procName)) return procName.trim { it <= ' ' } + } catch (e: java.lang.Exception) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + } finally { + if (reader != null) { + try { + reader.close() + } catch (e: java.lang.Exception) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + } + } + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + //try to fix SELinux limit due to unable access /proc file system + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + if (null != am) { + val appProcessInfoList = am.runningAppProcesses + if (null != appProcessInfoList) { + for (i in appProcessInfoList) { + if (i.pid == Process.myPid()) { + val result = i.processName.trim { it <= ' ' } + return result + } + } + } + } + } + + //Warnning: getApplicationInfo().processName only return package name for some reason, you will not see + // the real process name + return context.applicationInfo.processName + } + +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/ApiParamUtils.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/ApiParamUtils.kt new file mode 100644 index 0000000..7a60440 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/ApiParamUtils.kt @@ -0,0 +1,18 @@ +package guru.core.analytics.utils + +import guru.core.analytics.Constants +import guru.core.analytics.data.db.model.EventEntity +import guru.core.analytics.data.store.DeviceInfoStore + + +object ApiParamUtils { + + /** + * 组装接口上传需要的json参数 + */ + fun generateApiParam(events: List): String { + val deviceInfoJson = GsonUtil.gson.toJson(DeviceInfoStore.deviceInfo) + return "{\"version\":${Constants.VERSION},\"events\":[${events.joinToString(",") { it.json }}],\"deviceInfo\":$deviceInfoJson}" + } + +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/DateTimeUtils.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/DateTimeUtils.kt new file mode 100644 index 0000000..98ac741 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/DateTimeUtils.kt @@ -0,0 +1,26 @@ +package guru.core.analytics.utils + +import android.os.SystemClock +import java.util.Calendar +import java.util.Date + +object DateTimeUtils { + + const val DAYS_IN_MILLIS = 24 * 3600 * 1000 + + private var timeDifference: Long = 0L + + fun eventAt(): Long = + if (timeDifference > 0L) timeDifference + SystemClock.elapsedRealtime() else System.currentTimeMillis() + + fun initServerTime(lastServiceTime: Long): Long { + // Record the time difference + timeDifference = lastServiceTime - SystemClock.elapsedRealtime() + return lastServiceTime + } + + + fun now(): Date { + return Date(System.currentTimeMillis()) + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/EventChecker.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/EventChecker.kt new file mode 100644 index 0000000..b311e24 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/EventChecker.kt @@ -0,0 +1,9 @@ +package guru.core.analytics.utils + +object EventChecker { + private val nonAlphaNumeric: Regex = Regex("[^a-zA-Z0-9_]") + + fun isAlphabet(ch: Char): Boolean = ch in 'a'..'z' || ch in 'A'..'Z' + + fun containsNonAlphaNumeric(name: String): Boolean = nonAlphaNumeric.matches(name) +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/GZipUtils.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/GZipUtils.kt new file mode 100644 index 0000000..ffd5c00 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/GZipUtils.kt @@ -0,0 +1,26 @@ +package guru.core.analytics.utils + +import guru.core.analytics.handler.AnalyticsCode +import guru.core.analytics.handler.EventHandler +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.zip.GZIPOutputStream + +object GZipUtils { + + fun compress(str: String?): ByteArray? { + if (str.isNullOrBlank()) return null + return try { + val byteData = str.trim().toByteArray(charset("UTF-8")) + val out = ByteArrayOutputStream() + val gzip = GZIPOutputStream(out) + gzip.write(byteData) + gzip.close() + out.toByteArray() + } catch (e: IOException) { + EventHandler.INSTANCE.notifyEventHandler(AnalyticsCode.ERROR_ZIP, e.message) + null + } + } + +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/GsonUtil.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/GsonUtil.kt new file mode 100644 index 0000000..9f303b3 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/GsonUtil.kt @@ -0,0 +1,12 @@ +package guru.core.analytics.utils + +import com.google.gson.Gson +import com.google.gson.GsonBuilder + +object GsonUtil { + val gson: Gson by lazy { + return@lazy GsonBuilder() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .create() + } +} diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/SystemProperties.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/SystemProperties.kt new file mode 100644 index 0000000..568254f --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/SystemProperties.kt @@ -0,0 +1,38 @@ +package guru.core.analytics.utils + +import android.util.Log +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader + +object SystemProperties { + private const val GETPROP_EXECUTABLE_PATH = "/system/bin/getprop" + private const val TAG = "SystemProperties" + + fun read(propName: String): String { + var process: Process? = null + var bufferedReader: BufferedReader? = null + return try { + process = ProcessBuilder().command(GETPROP_EXECUTABLE_PATH, propName) + .redirectErrorStream(true).start() + bufferedReader = BufferedReader(InputStreamReader(process.inputStream)) + var line: String? = bufferedReader.readLine() + if (line == null) { + line = "" //prop not set + } + Log.i(TAG, "read System Property: $propName=$line") + line + } catch (e: Throwable) { + Log.e(TAG, "Failed to read System Property $propName", e) + "" + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close() + } catch (e: IOException) { + } + } + process?.destroy() + } + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/ZipUtils.kt b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/ZipUtils.kt new file mode 100644 index 0000000..73dbbae --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/java/guru/core/analytics/utils/ZipUtils.kt @@ -0,0 +1,162 @@ +package guru.core.analytics.utils + +import guru.core.analytics.ext.closeQuietly +import timber.log.Timber +import java.io.* +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +object ZipUtils { + private const val BUFFER_SIZE = 8192 + + /** + * Zips a file at a location and places the resulting zip file at the toLocation + * Example: zip("downloads/myfolder", "downloads/myfolder.zip"); + */ + fun zip(sourcePath: String, destFile: String): Boolean { + val sourceFile = File(sourcePath) + var origin: BufferedInputStream? = null + var out: ZipOutputStream? = null + + try { + out = ZipOutputStream(BufferedOutputStream(FileOutputStream(destFile))) + if (sourceFile.isDirectory) { + val entry = ZipEntry(getLastPathComponent(sourceFile.absolutePath) + "/") + out.putNextEntry(entry) + zipSubFolder(out, sourceFile, sourceFile.parent!!.length + 1) + + } else { + val data = ByteArray(BUFFER_SIZE) + origin = BufferedInputStream(FileInputStream(sourcePath), BUFFER_SIZE) + val entry = ZipEntry(getLastPathComponent(sourcePath)) + out.putNextEntry(entry) + + var count: Int + while (origin.read(data, 0, BUFFER_SIZE).also { count = it } != -1) { + out.write(data, 0, count) + } + } + + out.flush() + return true + } catch (e: Exception) { + Timber.e(e, "Zip exception") + } finally { + origin.closeQuietly() + out.closeQuietly() + } + return false + } + + @Throws(IOException::class) + private fun zipSubFolder(out: ZipOutputStream, folder: File, basePathLength: Int) { + val fileList: Array = folder.listFiles() ?: emptyArray() + for (file in fileList) { + if (file.isDirectory) { + val unmodifiedFilePath = file.path + val relativePath = unmodifiedFilePath.substring(basePathLength) + val entry = ZipEntry("$relativePath/") + out.putNextEntry(entry) + zipSubFolder(out, file, basePathLength) + + } else { + val data = ByteArray(BUFFER_SIZE) + val unmodifiedFilePath = file.path + val relativePath = unmodifiedFilePath.substring(basePathLength) + + var fi: FileInputStream? = null + var origin: BufferedInputStream? = null + try { + fi = FileInputStream(unmodifiedFilePath) + origin = BufferedInputStream(fi, BUFFER_SIZE) + + val entry = ZipEntry(relativePath) + out.putNextEntry(entry) + var count: Int + while (origin.read(data, 0, BUFFER_SIZE).also { count = it } != -1) { + out.write(data, 0, count) + } + out.flush() + } finally { + fi.closeQuietly() + origin.closeQuietly() + } + } + } + } + + /** + * gets the last path component + * + * Example: getLastPathComponent("downloads/example/fileToZip"); + * Result: "fileToZip" + */ + private fun getLastPathComponent(filePath: String): String { + val segments = filePath.split("/").toTypedArray() + return if (segments.isEmpty()) "" else segments[segments.size - 1] + } + + /** + * Unzip a zip file. Will overwrite existing files. + * + * @param zipFile Full path of the zip file you'd like to unzip. + * @param destPath Full path of the directory you'd like to unzip to (will be created if it doesn't exist). + */ + fun unzip(zipFile: String, destPath: String): Boolean { + var location = destPath + if (!location.endsWith("/")) { + location += "/" + } + + val f = File(location) + if (!f.isDirectory) { + f.mkdirs() + } + + var zin: ZipInputStream? = null + try { + zin = ZipInputStream(BufferedInputStream(FileInputStream(zipFile), BUFFER_SIZE)) + + var ze: ZipEntry + while (zin.nextEntry.also { ze = it } != null) { + + val path = location + ze.name + val unzipFile = File(path) + if (ze.isDirectory) { + if (!unzipFile.isDirectory) { + unzipFile.mkdirs() + } + + } else { // check for and create parent directories if they don't exist + val parentDir = unzipFile.parentFile + if (parentDir?.isDirectory == false) { + parentDir.mkdirs() + } + + var fout: BufferedOutputStream? = null + try { // unzip the file + fout = BufferedOutputStream(FileOutputStream(unzipFile, false), BUFFER_SIZE) + var size: Int + val buffer = ByteArray(BUFFER_SIZE) + + while (zin.read(buffer, 0, BUFFER_SIZE).also { size = it } != -1) { + fout.write(buffer, 0, size) + } + + fout.flush() + zin.closeEntry() + } finally { + fout.closeQuietly() + } + } + } + return true + } catch (ioe: IOException) { + Timber.e(ioe, "Unzip exception") + } finally { + zin.closeQuietly() + } + return false + } +} \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/res/values/colors.xml b/guru_analytics/guru_analytics/src/main/res/values/colors.xml new file mode 100644 index 0000000..55344e5 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/res/values/colors.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/res/values/strings.xml b/guru_analytics/guru_analytics/src/main/res/values/strings.xml new file mode 100644 index 0000000..e5f8fdc --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/main/res/values/themes.xml b/guru_analytics/guru_analytics/src/main/res/values/themes.xml new file mode 100644 index 0000000..2a710f3 --- /dev/null +++ b/guru_analytics/guru_analytics/src/main/res/values/themes.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/guru_analytics/guru_analytics/src/test/java/guru/data/ExampleUnitTest.kt b/guru_analytics/guru_analytics/src/test/java/guru/data/ExampleUnitTest.kt new file mode 100644 index 0000000..1bb5e1a --- /dev/null +++ b/guru_analytics/guru_analytics/src/test/java/guru/data/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package guru.data + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/guru_analytics/settings.gradle b/guru_analytics/settings.gradle new file mode 100644 index 0000000..3fa80a1 --- /dev/null +++ b/guru_analytics/settings.gradle @@ -0,0 +1,22 @@ +//pluginManagement { +// repositories { +// gradlePluginPortal() +// google() +// mavenLocal() +// mavenCentral() +// maven { url 'https://jitpack.io' } +// } +//} +//dependencyResolutionManagement { +// repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) +// repositories { +// google() +// mavenLocal() +// mavenCentral() +// // todo LoggingInterceptor +// maven { url 'https://jitpack.io' } +// } +//} +rootProject.name = "GuruAnalytics" +include ':app' +include ':guru_analytics' diff --git a/guru_checker/.gitignore b/guru_checker/.gitignore new file mode 100644 index 0000000..bb2a2bd --- /dev/null +++ b/guru_checker/.gitignore @@ -0,0 +1,14 @@ +.gradle +/captures +/local.properties +/.idea/workspace.xml +.DS_Store +/build +.idea/ +*iml +*.iml +*/build +/lib +wh.properties +/atom +*.txt \ No newline at end of file diff --git a/guru_checker/GuruChecker/.gitignore b/guru_checker/GuruChecker/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/guru_checker/GuruChecker/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/guru_checker/GuruChecker/build.gradle b/guru_checker/GuruChecker/build.gradle new file mode 100644 index 0000000..98b5d74 --- /dev/null +++ b/guru_checker/GuruChecker/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'maven-publish' +} + +apply from: 'maven-publish.gradle' + + +android { + namespace 'guru.core.checker' + compileSdk 33 + + defaultConfig { + minSdk 21 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} \ No newline at end of file diff --git a/guru_checker/GuruChecker/consumer-rules.pro b/guru_checker/GuruChecker/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/guru_checker/GuruChecker/maven-publish.gradle b/guru_checker/GuruChecker/maven-publish.gradle new file mode 100644 index 0000000..a44c133 --- /dev/null +++ b/guru_checker/GuruChecker/maven-publish.gradle @@ -0,0 +1,69 @@ +publishing { // Repositories *to* which Gradle can publish artifacts + repositories { RepositoryHandler handler -> + handler.mavenLocal() + maven { + url "$buildDir/repo" // change to point to your repo, e.g. http://my.org/repo + } + } + publications { PublicationContainer publication -> + + // Creates a Maven publication called "myPublication". + maven(MavenPublication) { + groupId 'guru.core.checker' + artifactId 'GuruChecker' + version '1.0.0' // Your package version +// artifact publishArtifact //Example: *./target/myJavaClasses.jar* +// artifact "build/outputs/aar/aar-test-release.aar"//aar包的目录 + afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) } // 方式一:生成aar包 + + //带上依赖 ,否则会报错 + pom.withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + def scopes = [] + if (configurations.hasProperty("api")) { + scopes.add(configurations.api) + } + if (configurations.hasProperty("implementation")) { + scopes.add(configurations.implementation) + } + if (configurations.hasProperty("debugImplementation")) { + scopes.add(configurations.debugImplementation) + } + if (configurations.hasProperty("releaseImplementation")) { + scopes.add(configurations.releaseImplementation) + } + +// if (project.ext.targetType != "jar") { +// scopes.add(configurations.provided) +// } + + scopes.each { scope -> + scope.allDependencies.each { + if (it instanceof ModuleDependency) { + boolean isTransitive = ((ModuleDependency) it).transitive + if (!isTransitive) { + println "<<<< not transitive dependency: [${it.group}, ${it.name}, ${it.version}]" + return + } + } + + if (it.group == "${project.rootProject.name}.libs" || it.version == 'unspecified') { + return + } + + if (it.group && it.name && it.version) { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + dependencyNode.appendNode('scope', scope.name) + } + } + } + } + } + } + + +} \ No newline at end of file diff --git a/guru_checker/GuruChecker/proguard-rules.pro b/guru_checker/GuruChecker/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/guru_checker/GuruChecker/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/guru_checker/GuruChecker/src/androidTest/java/guru/core/checker/ExampleInstrumentedTest.kt b/guru_checker/GuruChecker/src/androidTest/java/guru/core/checker/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d1721b1 --- /dev/null +++ b/guru_checker/GuruChecker/src/androidTest/java/guru/core/checker/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package guru.core.checker + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("guru.core.checker.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/guru_checker/GuruChecker/src/main/AndroidManifest.xml b/guru_checker/GuruChecker/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/guru_checker/GuruChecker/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/guru_checker/GuruChecker/src/main/java/guru/core/checker/GuruChecker.kt b/guru_checker/GuruChecker/src/main/java/guru/core/checker/GuruChecker.kt new file mode 100644 index 0000000..493a28b --- /dev/null +++ b/guru_checker/GuruChecker/src/main/java/guru/core/checker/GuruChecker.kt @@ -0,0 +1,16 @@ +package guru.core.checker + +import android.content.Context +import android.content.pm.PackageManager + +object GuruChecker { + + fun isAppInstalled(context: Context, packageName: String): Boolean { + val packageManager: PackageManager = context.packageManager + return try { + packageManager.getLaunchIntentForPackage(packageName) != null + } catch (e: PackageManager.NameNotFoundException) { + false + } + } +} \ No newline at end of file diff --git a/guru_checker/GuruChecker/src/test/java/guru/core/checker/ExampleUnitTest.kt b/guru_checker/GuruChecker/src/test/java/guru/core/checker/ExampleUnitTest.kt new file mode 100644 index 0000000..da3b274 --- /dev/null +++ b/guru_checker/GuruChecker/src/test/java/guru/core/checker/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package guru.core.checker + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/guru_checker/app/.gitignore b/guru_checker/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/guru_checker/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/guru_checker/app/build.gradle b/guru_checker/app/build.gradle new file mode 100644 index 0000000..3c5c231 --- /dev/null +++ b/guru_checker/app/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'guru.app.guruchecker' + compileSdk 33 + + defaultConfig { + applicationId "guru.app.guruchecker" + minSdk 21 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.google.android.material:material:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation project(path: ':GuruChecker') + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + implementation 'guru.core.checker:GuruChecker:1.0.0' + +} \ No newline at end of file diff --git a/guru_checker/app/proguard-rules.pro b/guru_checker/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/guru_checker/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/guru_checker/app/src/androidTest/java/guru/app/guruchecker/ExampleInstrumentedTest.kt b/guru_checker/app/src/androidTest/java/guru/app/guruchecker/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b890f8d --- /dev/null +++ b/guru_checker/app/src/androidTest/java/guru/app/guruchecker/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package guru.app.guruchecker + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("guru.app.guruchecker", appContext.packageName) + } +} \ No newline at end of file diff --git a/guru_checker/app/src/main/AndroidManifest.xml b/guru_checker/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7873073 --- /dev/null +++ b/guru_checker/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/guru_checker/app/src/main/java/guru/app/guruchecker/MainActivity.kt b/guru_checker/app/src/main/java/guru/app/guruchecker/MainActivity.kt new file mode 100644 index 0000000..27f8602 --- /dev/null +++ b/guru_checker/app/src/main/java/guru/app/guruchecker/MainActivity.kt @@ -0,0 +1,15 @@ +package guru.app.guruchecker +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import guru.core.checker.GuruChecker + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + Log.d("MainActivity", "onCreate:${GuruChecker.isAppInstalled(this, "merge.blocks.drop.number.puzzle.games")}") + Log.d("MainActivity", "onCreate:${GuruChecker.isAppInstalled(this, "sudoku.puzzle.free.game.brain")}") + } +} \ No newline at end of file diff --git a/guru_checker/app/src/main/res/drawable/ic_launcher_background.xml b/guru_checker/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/guru_checker/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guru_checker/app/src/main/res/drawable/ic_launcher_foreground.xml b/guru_checker/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/guru_checker/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/guru_checker/app/src/main/res/layout/activity_main.xml b/guru_checker/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..17eab17 --- /dev/null +++ b/guru_checker/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/guru_checker/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/guru_checker/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/guru_checker/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/guru_checker/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/guru_checker/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/guru_checker/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/guru_checker/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/guru_checker/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/guru_checker/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/guru_checker/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/guru_checker/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/guru_checker/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/guru_checker/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/guru_checker/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/guru_checker/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/guru_checker/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/guru_checker/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/guru_checker/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/guru_checker/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/guru_checker/app/src/main/res/values/colors.xml b/guru_checker/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/guru_checker/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/guru_checker/app/src/main/res/values/strings.xml b/guru_checker/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..e64bb83 --- /dev/null +++ b/guru_checker/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + GuruChecker + \ No newline at end of file diff --git a/guru_checker/app/src/main/res/values/themes.xml b/guru_checker/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..fa4d073 --- /dev/null +++ b/guru_checker/app/src/main/res/values/themes.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/guru_checker/app/src/main/res/xml/backup_rules.xml b/guru_checker/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/guru_checker/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/guru_checker/app/src/main/res/xml/data_extraction_rules.xml b/guru_checker/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/guru_checker/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/guru_checker/app/src/test/java/guru/app/guruchecker/ExampleUnitTest.kt b/guru_checker/app/src/test/java/guru/app/guruchecker/ExampleUnitTest.kt new file mode 100644 index 0000000..a6662a5 --- /dev/null +++ b/guru_checker/app/src/test/java/guru/app/guruchecker/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package guru.app.guruchecker + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/guru_checker/build.gradle b/guru_checker/build.gradle new file mode 100644 index 0000000..ca7e379 --- /dev/null +++ b/guru_checker/build.gradle @@ -0,0 +1,6 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.2.1' apply false + id 'com.android.library' version '7.2.1' apply false + id 'org.jetbrains.kotlin.android' version '1.7.10' apply false +} \ No newline at end of file diff --git a/guru_checker/gradle.properties b/guru_checker/gradle.properties new file mode 100644 index 0000000..3c5031e --- /dev/null +++ b/guru_checker/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/guru_checker/gradle/wrapper/gradle-wrapper.jar b/guru_checker/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/guru_checker/gradle/wrapper/gradle-wrapper.jar differ diff --git a/guru_checker/gradle/wrapper/gradle-wrapper.properties b/guru_checker/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1fc1215 --- /dev/null +++ b/guru_checker/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Sep 11 18:10:08 CST 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/guru_checker/gradlew b/guru_checker/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/guru_checker/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/guru_checker/gradlew.bat b/guru_checker/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/guru_checker/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/guru_checker/settings.gradle b/guru_checker/settings.gradle new file mode 100644 index 0000000..456978a --- /dev/null +++ b/guru_checker/settings.gradle @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + mavenLocal() + + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + mavenLocal() + } +} +rootProject.name = "GuruChecker" +include ':app' +include ':GuruChecker'