diff --git a/android-sdk/build.gradle b/android-sdk/build.gradle index 962fd357..dd316c85 100644 --- a/android-sdk/build.gradle +++ b/android-sdk/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { compileSdkVersion compile_sdk_version @@ -28,6 +27,7 @@ android { multiDexEnabled true testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' buildConfigField "String", "CLIENT_VERSION", "\"$version_name\"" + multiDexEnabled true // these rules will be merged to app's proguard rules consumerProguardFiles '../proguard-rules.txt' @@ -48,10 +48,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - - dexOptions { - javaMaxHeapSize "4g" - } } dependencies { diff --git a/build.gradle b/build.gradle index e471cd4b..7e557c4a 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -86,7 +86,7 @@ task clean(type: Delete) { task cleanAllModules () { logger.info("Running clean for all modules") dependsOn(':android-sdk:clean', ':event-handler:clean', - ':user-profile:clean', ':shared:clean', ':datafile-handler:clean') + ':user-profile:clean', ':shared:clean', ':datafile-handler:clean', ':odp:clean') } task testAllModules () { @@ -100,20 +100,21 @@ task testAllModulesTravis () { ':event-handler:connectedAndroidTest', ':event-handler:test', ':datafile-handler:connectedAndroidTest', ':datafile-handler:test', ':user-profile:connectedAndroidTest', - ':shared:connectedAndroidTest') + ':shared:connectedAndroidTest', + ':odp:connectedAndroidTest', ':odp:test' + ) } // Publish to MavenCentral task ship() { - dependsOn(':android-sdk:ship', ':shared:ship', ':event-handler:ship', ':user-profile:ship', ':datafile-handler:ship') + dependsOn(':android-sdk:ship', ':shared:ship', ':event-handler:ship', ':user-profile:ship', ':datafile-handler:ship', ':odp:ship') } def publishedProjects = subprojects.findAll { it.name != 'test-app' } configure(publishedProjects) { apply plugin: 'com.android.library' apply plugin: 'kotlin-android' - apply plugin: 'kotlin-android-extensions' apply plugin: 'maven-publish' apply plugin: 'signing' @@ -140,13 +141,17 @@ configure(publishedProjects) { artifactName = 'android-sdk-user-profile' docTitle = 'Optimizely X Android SDK: User Profile' break + case 'odp': + artifactName = 'android-sdk-odp' + docTitle = 'Optimizely X Android SDK: ODP' + break default: return } android.libraryVariants.all { variant -> task("${variant.name}Javadoc", type: Javadoc, dependsOn: "assemble${variant.name.capitalize()}") { - source = variant.javaCompile.source + source = variant.javaCompileProvider.get().source title = docTitle @@ -178,11 +183,20 @@ configure(publishedProjects) { android.libraryVariants.all { variant -> task("${variant.name}SourcesJar", type: Jar) { classifier = 'sources' - from variant.javaCompile.source + from variant.javaCompileProvider.get().source } project.artifacts.add("archives", tasks["${variant.name}SourcesJar"]); } + android { + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } + } + afterEvaluate { publishing { publications { @@ -196,8 +210,6 @@ configure(publishedProjects) { pom.description = 'The Android SDK for Optimizely Full Stack (feature flag management for product development teams)' from components.release - artifact releaseSourcesJar - artifact releaseJavadocJar } } repositories { @@ -232,11 +244,12 @@ configure(publishedProjects) { } } -project(':android-sdk').ship.shouldRunAfter = [':android-sdk:clean', ':datafile-handler:ship', ':event-handler:ship', ':user-profile:ship'] +project(':android-sdk').ship.shouldRunAfter = [':android-sdk:clean', ':datafile-handler:ship', ':event-handler:ship', ':user-profile:ship', ':odp:ship'] project(':datafile-handler').ship.shouldRunAfter = [':datafile-handler:clean', ':shared:ship'] project(':event-handler').ship.shouldRunAfter = [':event-handler:clean', ':shared:ship'] project(':shared').ship.shouldRunAfter = [':shared:clean'] project(':user-profile').ship.shouldRunAfter = [':user-profile:clean', ':shared:ship'] +project(':odp').ship.shouldRunAfter = [':odp:clean', ':shared:ship'] // standard POM format required by MavenCentral diff --git a/datafile-handler/build.gradle b/datafile-handler/build.gradle index d6393166..6cdbfc68 100644 --- a/datafile-handler/build.gradle +++ b/datafile-handler/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { compileSdkVersion compile_sdk_version diff --git a/event-handler/build.gradle b/event-handler/build.gradle index d11d6cac..b57ac3f2 100644 --- a/event-handler/build.gradle +++ b/event-handler/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { compileSdkVersion compile_sdk_version diff --git a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventDAOTest.java b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventDAOTest.java index ad96c53f..340daa0c 100644 --- a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventDAOTest.java +++ b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventDAOTest.java @@ -59,7 +59,7 @@ public void setupEventDAO() { @After public void tearDownEventDAO() { - assertTrue(context.deleteDatabase(String.format(EventSQLiteOpenHelper.DB_NAME , "1"))); + context.deleteDatabase(String.format(EventSQLiteOpenHelper.DB_NAME , "1")); } @Test @@ -80,7 +80,7 @@ public void getEvents() throws MalformedURLException { assertTrue(eventDAO.storeEvent(event3)); List> events = eventDAO.getEvents(); - assertTrue(events.size() == 3); + assertEquals(events.size(), 3); Pair pair1 = events.get(0); Pair pair2 = events.get(1); diff --git a/gradle.properties b/gradle.properties index ffde98ef..f2a434a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1g +android.disableAutomaticComponentCreation=true \ No newline at end of file diff --git a/odp/.gitignore b/odp/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/odp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/odp/build.gradle b/odp/build.gradle new file mode 100644 index 00000000..e1f0ade6 --- /dev/null +++ b/odp/build.gradle @@ -0,0 +1,76 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// 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. +// + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion compile_sdk_version + buildToolsVersion build_tools_version + + defaultConfig { + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version + versionCode 1 + versionName "1.0" + multiDexEnabled true + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + testOptions { + unitTests.returnDefaultValues = true + } + buildTypes { + release { + minifyEnabled false + } + debug { + testCoverageEnabled true + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildToolsVersion build_tools_version + + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + api project(':shared') + implementation "androidx.annotation:annotation:$annotations_ver" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.test.ext:junit-ktx:1.1.3' + + testImplementation "junit:junit:$junit_ver" + testImplementation "org.mockito:mockito-core:$mockito_ver" + + compileOnly "com.noveogroup.android:android-logger:$android_logger_ver" + + androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit" + androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_ver" + // Set this dependency to use JUnit 4 rules + androidTestImplementation "androidx.test:rules:$androidx_test_rules" + androidTestImplementation "androidx.test:core:$androidx_test_core" + androidTestImplementation "androidx.test:core-ktx:$androidx_test_core" + + androidTestImplementation "org.mockito:mockito-core:$mockito_ver" + androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver" + androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver" + androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver" +} \ No newline at end of file diff --git a/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt b/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt new file mode 100644 index 00000000..25baf27e --- /dev/null +++ b/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt @@ -0,0 +1,107 @@ +// Copyright 2022, Optimizely, Inc. and contributors +// +// 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. + +package com.optimizely.ab.android.odp + +import android.content.Context +import android.content.SharedPreferences +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class VuidManagerTest { + lateinit var vuidManager: VuidManager + val context = getInstrumentation().getTargetContext() + + @Before + fun setUp() { + // remove vuid storage + cleanSharedPrefs() + // remove a singlton instance + VuidManager.removeSharedForTesting() + + vuidManager = VuidManager.getShared(context) + } + + @After + fun cleanSharedPrefs() { + val sharedPreferences: SharedPreferences = context.getSharedPreferences("optly", Context.MODE_PRIVATE) + val editor = sharedPreferences.edit() + editor.clear() + editor.commit() + } + + @Test + fun makeVuid() { + val vuid = vuidManager.makeVuid() + assertTrue(vuid.length == 32) + assertTrue(vuid.startsWith("vuid_", ignoreCase = false)) + assertTrue("generated vuids should be all lowercased", vuid.toLowerCase().equals(vuid, ignoreCase = false)) + } + + @Test + fun isVuid() { + assertTrue(VuidManager.isVuid("vuid_1234")) + assertTrue(VuidManager.isVuid("VUID_1234")) + assertFalse(VuidManager.isVuid("vuid1234")) + assertFalse(VuidManager.isVuid("1234")) + assertFalse(VuidManager.isVuid("")) + } + + @Test + fun loadBeforeSave() { + val vuid1 = vuidManager.load(context) + assertTrue("new vuid is created", VuidManager.isVuid(vuid1)) + val vuid2 = vuidManager.load(context) + assertEquals("old vuid should be returned since load will save a created vuid", vuid1, vuid2) + } + + @Test + fun loadAfterSave() { + vuidManager.save(context,"vuid_1234") + val vuidLoaded = vuidManager.load(context) + assertEquals("saved vuid should be returned", vuidLoaded, "vuid_1234") + val vuidLoaded2 = vuidManager.load(context) + assertEquals("the same vuid should be returned", vuidLoaded2, "vuid_1234") + } + + @Test + fun autoLoaded() { + val vuid1 = VuidManager.getShared(context).vuid + assertTrue("vuid should be auto loaded when constructed", vuid1.startsWith("vuid_")) + + val vuid2 = VuidManager.getShared(context).vuid + assertEquals("the same vuid should be returned when getting a singleton", vuid1, vuid2) + + // remove shared instance, so will be re-instantiated + VuidManager.removeSharedForTesting() + + val vuid3 = VuidManager.getShared(context).vuid + assertEquals("the saved vuid should be returned when instantiated again", vuid2, vuid3) + + // remove saved vuid + cleanSharedPrefs() + // remove shared instance, so will be re-instantiated + VuidManager.removeSharedForTesting() + + val vuid4 = VuidManager.getShared(context).vuid + assertNotEquals("a new vuid should be returned when storage cleared and re-instantiated", vuid3, vuid4) + assertTrue(vuid4.startsWith("vuid_")) + } +} \ No newline at end of file diff --git a/odp/src/main/AndroidManifest.xml b/odp/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b546495f --- /dev/null +++ b/odp/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt b/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt new file mode 100644 index 00000000..ffb891c7 --- /dev/null +++ b/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt @@ -0,0 +1,72 @@ +// Copyright 2022, Optimizely, Inc. and contributors +// +// 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. + +package com.optimizely.ab.android.odp + +import android.content.Context +import androidx.annotation.VisibleForTesting +import com.optimizely.ab.android.shared.OptlyStorage +import java.util.UUID + +class VuidManager private constructor(context: Context) { + var vuid = "" + private val keyForVuid = "vuid" // stored in the private "optly" storage + + init { + this.vuid = load(context) + } + + companion object { + @Volatile + private var INSTANCE: VuidManager? = null + + @Synchronized + fun getShared(context: Context): VuidManager = INSTANCE ?: VuidManager(context).also { INSTANCE = it } + + fun isVuid(visitorId: String): Boolean { + return visitorId.startsWith("vuid_", ignoreCase = true) + } + + @VisibleForTesting + fun removeSharedForTesting() { + INSTANCE = null + } + } + + @VisibleForTesting + fun makeVuid(): String { + val maxLength = 32 // required by ODP server + + // make sure UUIDv4 is used (not UUIDv1 or UUIDv6) since the trailing 5 chars will be truncated. See TDD for details. + val vuidFull = "vuid_" + UUID.randomUUID().toString().replace("-", "").lowercase() + return if (vuidFull.length <= maxLength) vuidFull else vuidFull.substring(0, maxLength) + } + + @VisibleForTesting + fun load(context: Context): String { + val storage = OptlyStorage(context) + val oldVuid = storage.getString(keyForVuid, null) + if (oldVuid != null) return oldVuid + + val vuid = makeVuid() + save(context, vuid) + return vuid + } + + @VisibleForTesting + fun save(context: Context, vuid: String) { + val storage = OptlyStorage(context) + storage.saveString(keyForVuid, vuid) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a53a043c..1cfea7d7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':event-handler', ':android-sdk', ':shared', ':test-app', ':user-profile', ':datafile-handler' +include ':event-handler', ':android-sdk', ':shared', ':test-app', ':user-profile', ':datafile-handler', ':odp' diff --git a/shared/build.gradle b/shared/build.gradle index 624ff4a4..93a7e1ef 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { compileSdkVersion compile_sdk_version diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java b/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java index b1d2dafe..260acbb7 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java @@ -62,6 +62,28 @@ public long getLong(String key, long defaultValue) { return getReadablePrefs().getLong(key, defaultValue); } + /** + * Save a string value + * + * @param key a String key + * @param value a string value + * @hide + */ + public void saveString(String key, String value) { + getWritablePrefs().putString(key, value).apply(); + } + + /** + * Get a string value + * @param key a String key + * @param defaultValue the value to return if the key isn't stored + * @return the string value + * @hide + */ + public String getString(String key, String defaultValue) { + return getReadablePrefs().getString(key, defaultValue); + } + private SharedPreferences.Editor getWritablePrefs() { return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit(); } diff --git a/test-app/build.gradle b/test-app/build.gradle index ef448760..01b3d1c4 100644 --- a/test-app/build.gradle +++ b/test-app/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { compileSdkVersion compile_sdk_version @@ -8,7 +7,7 @@ android { defaultConfig { - minSdkVersion 14 + minSdkVersion 21 targetSdkVersion target_sdk_version versionCode 1 versionName "1.0.2" diff --git a/user-profile/build.gradle b/user-profile/build.gradle index 56ecc5a9..cfc7fee3 100644 --- a/user-profile/build.gradle +++ b/user-profile/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { compileSdkVersion compile_sdk_version @@ -49,6 +48,7 @@ dependencies { api project(':shared') implementation "androidx.annotation:annotation:$annotations_ver" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" compileOnly "com.noveogroup.android:android-logger:$android_logger_ver" @@ -70,5 +70,4 @@ dependencies { androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver" androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver" androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }