Panagiotis Triantafyllou

migrate to maven central, db fixes for android 15

1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <project version="4"> 2 <project version="4">
3 <component name="CompilerConfiguration"> 3 <component name="CompilerConfiguration">
4 - <bytecodeTargetLevel target="17" /> 4 + <bytecodeTargetLevel target="21" />
5 </component> 5 </component>
6 </project> 6 </project>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
3 <component name="GradleSettings"> 3 <component name="GradleSettings">
4 <option name="linkedExternalProjectsSettings"> 4 <option name="linkedExternalProjectsSettings">
5 <GradleProjectSettings> 5 <GradleProjectSettings>
6 + <option name="testRunner" value="CHOOSE_PER_TEST" />
6 <option name="externalProjectPath" value="$PROJECT_DIR$" /> 7 <option name="externalProjectPath" value="$PROJECT_DIR$" />
7 <option name="gradleJvm" value="jbr-17" /> 8 <option name="gradleJvm" value="jbr-17" />
8 <option name="modules"> 9 <option name="modules">
......
1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <project version="4"> 2 <project version="4">
3 <component name="ExternalStorageConfigurationManager" enabled="true" /> 3 <component name="ExternalStorageConfigurationManager" enabled="true" />
4 - <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> 4 + <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
5 <output url="file://$PROJECT_DIR$/build/classes" /> 5 <output url="file://$PROJECT_DIR$/build/classes" />
6 </component> 6 </component>
7 </project> 7 </project>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -2,13 +2,14 @@ apply plugin: 'com.android.application' ...@@ -2,13 +2,14 @@ apply plugin: 'com.android.application'
2 apply plugin: 'com.google.gms.google-services' 2 apply plugin: 'com.google.gms.google-services'
3 3
4 android { 4 android {
5 - compileSdkVersion 33 5 + compileSdkVersion 35
6 - buildToolsVersion "33.0.2" 6 + buildToolsVersion "35.0.0"
7 7
8 + namespace "warp.ly.android_sdk"
8 defaultConfig { 9 defaultConfig {
9 applicationId "warp.ly.android_sdk" 10 applicationId "warp.ly.android_sdk"
10 minSdkVersion 23 11 minSdkVersion 23
11 - targetSdkVersion 33 12 + targetSdkVersion 35
12 versionCode 100 13 versionCode 100
13 versionName "1.0.0" 14 versionName "1.0.0"
14 } 15 }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 Uuid=b13ade8ef743468b89a7aaa8efbfc468 4 Uuid=b13ade8ef743468b89a7aaa8efbfc468
5 5
6 # If we need to see logs in Logcat 6 # If we need to see logs in Logcat
7 -Debug=false 7 +Debug=true
8 8
9 # Production or Development environment of the engage server 9 # Production or Development environment of the engage server
10 # Production: https://engage.warp.ly 10 # Production: https://engage.warp.ly
......
...@@ -8,16 +8,19 @@ buildscript { ...@@ -8,16 +8,19 @@ buildscript {
8 maven { url 'https://plugins.gradle.org/m2/' } 8 maven { url 'https://plugins.gradle.org/m2/' }
9 } 9 }
10 dependencies { 10 dependencies {
11 - classpath 'com.android.tools.build:gradle:7.0.4' 11 + classpath 'com.android.tools.build:gradle:8.7.3'
12 - classpath 'com.google.gms:google-services:4.3.15' 12 + classpath 'com.google.gms:google-services:4.4.3'
13 - classpath 'com.huawei.agconnect:agcp:1.7.2.300' 13 + classpath 'com.huawei.agconnect:agcp:1.9.3.300'
14 - classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
15 14
16 // NOTE: Do not place your application dependencies here; they belong 15 // NOTE: Do not place your application dependencies here; they belong
17 // in the individual module build.gradle files 16 // in the individual module build.gradle files
18 } 17 }
19 } 18 }
20 19
20 +plugins {
21 + id 'maven-publish'
22 +}
23 +
21 allprojects { 24 allprojects {
22 repositories { 25 repositories {
23 mavenCentral() 26 mavenCentral()
...@@ -27,5 +30,4 @@ allprojects { ...@@ -27,5 +30,4 @@ allprojects {
27 } 30 }
28 } 31 }
29 32
30 -apply plugin: 'io.github.gradle-nexus.publish-plugin'
31 apply from: "${rootDir}/scripts/publish-root.gradle" 33 apply from: "${rootDir}/scripts/publish-root.gradle"
...\ No newline at end of file ...\ No newline at end of file
......
1 distributionBase=GRADLE_USER_HOME 1 distributionBase=GRADLE_USER_HOME
2 distributionPath=wrapper/dists 2 distributionPath=wrapper/dists
3 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 3 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
4 zipStoreBase=GRADLE_USER_HOME 4 zipStoreBase=GRADLE_USER_HOME
5 zipStorePath=wrapper/dists 5 zipStorePath=wrapper/dists
......
...@@ -32,15 +32,15 @@ afterEvaluate { ...@@ -32,15 +32,15 @@ afterEvaluate {
32 32
33 // Two artifacts, the `aar` (or `jar`) and the sources 33 // Two artifacts, the `aar` (or `jar`) and the sources
34 if (project.plugins.findPlugin("com.android.library")) { 34 if (project.plugins.findPlugin("com.android.library")) {
35 - from components.release 35 + from(project.components.findByName("release"))
36 } else { 36 } else {
37 from components.java 37 from components.java
38 } 38 }
39 39
40 - artifact androidSourcesJar 40 + // Sources are now handled by the android block's singleVariant
41 // artifact javadocJar 41 // artifact javadocJar
42 42
43 - // Mostly self-explanatory metadata 43 + // POM metadata for Maven Central
44 pom { 44 pom {
45 name = PUBLISH_ARTIFACT_ID 45 name = PUBLISH_ARTIFACT_ID
46 description = 'Warply Android SDK Maven Plugin' 46 description = 'Warply Android SDK Maven Plugin'
...@@ -60,8 +60,7 @@ afterEvaluate { ...@@ -60,8 +60,7 @@ afterEvaluate {
60 // Add all other devs here... 60 // Add all other devs here...
61 } 61 }
62 62
63 - // Version control info - if you're using GitHub, follow the 63 + // Version control info
64 - // format as seen here
65 scm { 64 scm {
66 connection = 'scm:git:git.warp.ly/open-source/warply_android_sdk_maven_plugin.git' 65 connection = 'scm:git:git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
67 developerConnection = 'scm:git:ssh://git.warp.ly/open-source/warply_android_sdk_maven_plugin.git' 66 developerConnection = 'scm:git:ssh://git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
...@@ -81,3 +80,365 @@ signing { ...@@ -81,3 +80,365 @@ signing {
81 ) 80 )
82 sign publishing.publications 81 sign publishing.publications
83 } 82 }
83 +
84 +// Configuration for Central Publishing (similar to Maven plugin configuration)
85 +ext.centralPublishing = [
86 + autoPublish : false, // Manual publishing for safety
87 + // waitUntil: "published", // Commented out - don't wait for publishing
88 + deploymentName: "Warply Android SDK ${PUBLISH_VERSION}",
89 + centralBaseUrl: "https://central.sonatype.com"
90 +]
91 +
92 +// Custom task that implements the same functionality as org.sonatype.central:central-publishing-maven-plugin:0.8.0
93 +// This uses the Central Portal API directly to achieve the same result
94 +task publishToCentralPortal {
95 + dependsOn 'publishReleasePublicationToMavenLocal'
96 +
97 + description = 'Publishes to Maven Central Portal using the same API as central-publishing-maven-plugin:0.8.0'
98 + group = 'publishing'
99 +
100 + doLast {
101 + def username = rootProject.ext["centralPortalUsername"]
102 + def password = rootProject.ext["centralPortalPassword"]
103 + def config = project.ext.centralPublishing
104 +
105 + if (!username || !password) {
106 + throw new GradleException("Central Portal credentials not configured. Please set centralPortalUsername and centralPortalPassword in local.properties or environment variables.")
107 + }
108 +
109 + println "=== Central Portal Publishing ==="
110 + println "Deployment: ${config.deploymentName}"
111 + println "Auto-publish: ${config.autoPublish}"
112 + println "Portal URL: ${config.centralBaseUrl}"
113 + println ""
114 +
115 + // Step 1: Create deployment bundle
116 + println "Step 1: Creating deployment bundle..."
117 + def bundleFile = createDeploymentBundle()
118 + println "✓ Bundle created: ${bundleFile.name} (${bundleFile.length()} bytes)"
119 +
120 + // Step 2: Upload bundle to Central Portal
121 + println "\nStep 2: Uploading to Central Portal..."
122 + def deploymentId = uploadBundle(bundleFile, username, password, config)
123 + println "✓ Upload successful. Deployment ID: ${deploymentId}"
124 +
125 + // Step 3: Wait for validation
126 + println "\nStep 3: Waiting for validation..."
127 + def validationResult = waitForValidation(deploymentId, username, password, config)
128 +
129 + if (validationResult.success) {
130 + def state = validationResult.state
131 + println "✓ Validation successful! State: ${state}"
132 +
133 + if (config.autoPublish && state == "VALIDATED") {
134 + println "\nStep 4: Auto-publishing..."
135 + def publishResult = publishDeployment(deploymentId, username, password, config)
136 + if (publishResult.success) {
137 + println "✓ Published successfully to Maven Central!"
138 + } else {
139 + throw new GradleException("Auto-publishing failed: ${publishResult.error}")
140 + }
141 + } else if (state == "PUBLISHED") {
142 + println "✓ Already published to Maven Central!"
143 + def response = validationResult.response
144 + def purls = response.purls ?: []
145 + if (purls) {
146 + println " Published artifacts:"
147 + purls.each { purl -> println " - ${purl}" }
148 + }
149 + } else {
150 + println "\n✓ Deployment uploaded and validated successfully!"
151 + println "📋 Manual action required:"
152 + println " Visit: ${config.centralBaseUrl}/publishing/deployments"
153 + println " Find deployment: ${config.deploymentName}"
154 + println " Click 'Publish' to complete the process"
155 + }
156 + } else {
157 + throw new GradleException("Validation failed: ${validationResult.error}")
158 + }
159 +
160 + println "\n=== Publishing Complete ==="
161 + }
162 +}
163 +
164 +def createDeploymentBundle() {
165 + def bundleDir = file("${buildDir}/central-publishing")
166 + def stagingDir = file("${bundleDir}/staging")
167 +
168 + // Clean and create directories
169 + bundleDir.deleteDir()
170 + stagingDir.mkdirs()
171 +
172 + // Create Maven repository structure
173 + def groupPath = PUBLISH_GROUP_ID.replace('.', '/')
174 + def artifactDir = file("${stagingDir}/${groupPath}/${PUBLISH_ARTIFACT_ID}/${PUBLISH_VERSION}")
175 + artifactDir.mkdirs()
176 +
177 + // Copy artifacts to staging area
178 + def artifacts = [:]
179 +
180 + // AAR file
181 + def aarFile = file("${buildDir}/outputs/aar/warply_android_sdk-release.aar")
182 + if (aarFile.exists()) {
183 + def targetAar = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.aar")
184 + copy {
185 + from aarFile
186 + into artifactDir
187 + rename { targetAar.name }
188 + }
189 + artifacts['aar'] = targetAar
190 +
191 + // Copy AAR signature if exists
192 + def aarSigFile = file("${aarFile.path}.asc")
193 + if (aarSigFile.exists()) {
194 + def targetAarSig = file("${targetAar.path}.asc")
195 + copy {
196 + from aarSigFile
197 + into artifactDir
198 + rename { targetAarSig.name }
199 + }
200 + artifacts['aar-sig'] = targetAarSig
201 + }
202 + }
203 +
204 + // Sources JAR
205 + def sourcesFile = file("${buildDir}/libs/warply_android_sdk-${PUBLISH_VERSION}-sources.jar")
206 + if (sourcesFile.exists()) {
207 + def targetSources = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}-sources.jar")
208 + copy {
209 + from sourcesFile
210 + into artifactDir
211 + rename { targetSources.name }
212 + }
213 + artifacts['sources'] = targetSources
214 +
215 + // Copy sources signature if exists
216 + def sourcesSigFile = file("${sourcesFile.path}.asc")
217 + if (sourcesSigFile.exists()) {
218 + def targetSourcesSig = file("${targetSources.path}.asc")
219 + copy {
220 + from sourcesSigFile
221 + into artifactDir
222 + rename { targetSourcesSig.name }
223 + }
224 + artifacts['sources-sig'] = targetSourcesSig
225 + }
226 + }
227 +
228 + // POM file
229 + def pomFile = file("${buildDir}/publications/release/pom-default.xml")
230 + if (pomFile.exists()) {
231 + def targetPom = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.pom")
232 + copy {
233 + from pomFile
234 + into artifactDir
235 + rename { targetPom.name }
236 + }
237 + artifacts['pom'] = targetPom
238 +
239 + // Copy POM signature if exists
240 + def pomSigFile = file("${pomFile.path}.asc")
241 + if (pomSigFile.exists()) {
242 + def targetPomSig = file("${targetPom.path}.asc")
243 + copy {
244 + from pomSigFile
245 + into artifactDir
246 + rename { targetPomSig.name }
247 + }
248 + artifacts['pom-sig'] = targetPomSig
249 + }
250 + }
251 +
252 + // Generate checksums for all files
253 + artifacts.each { type, artifactFile ->
254 + if (artifactFile.exists()) {
255 + generateChecksums(artifactFile)
256 + }
257 + }
258 +
259 + // Create bundle ZIP
260 + def bundleFile = file("${bundleDir}/central-bundle.zip")
261 + ant.zip(destfile: bundleFile) {
262 + fileset(dir: stagingDir)
263 + }
264 +
265 + return bundleFile
266 +}
267 +
268 +def generateChecksums(File file) {
269 + ['md5', 'sha1', 'sha256', 'sha512'].each { algorithm ->
270 + def checksum = file.withInputStream { stream ->
271 + java.security.MessageDigest.getInstance(algorithm.toUpperCase()).digest(stream.bytes).encodeHex().toString()
272 + }
273 + new File("${file.path}.${algorithm}").text = checksum
274 + }
275 +}
276 +
277 +def uploadBundle(File bundleFile, String username, String password, Map config) {
278 + def url = "${config.centralBaseUrl}/api/v1/publisher/upload"
279 + def credentials = "${username}:${password}".bytes.encodeBase64().toString()
280 +
281 + // Add query parameters
282 + def publishingType = config.autoPublish ? "AUTOMATIC" : "USER_MANAGED"
283 + def urlWithParams = "${url}?publishingType=${publishingType}&name=${URLEncoder.encode(config.deploymentName, 'UTF-8')}"
284 +
285 + println " Uploading to: ${urlWithParams}"
286 + println " Publishing type: ${publishingType}"
287 +
288 + def connection = new URL(urlWithParams).openConnection() as HttpURLConnection
289 + connection.setRequestMethod("POST")
290 + connection.setRequestProperty("Authorization", "Bearer ${credentials}")
291 + connection.setDoOutput(true)
292 + connection.setDoInput(true)
293 +
294 + // Create multipart/form-data boundary
295 + def boundary = "----WebKitFormBoundary" + System.currentTimeMillis()
296 + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=${boundary}")
297 +
298 + // Write multipart data
299 + connection.outputStream.withWriter("UTF-8") { writer ->
300 + writer.write("--${boundary}\r\n")
301 + writer.write("Content-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.name}\"\r\n")
302 + writer.write("Content-Type: application/octet-stream\r\n")
303 + writer.write("\r\n")
304 + writer.flush()
305 +
306 + // Write file content
307 + bundleFile.withInputStream { input ->
308 + connection.outputStream << input
309 + }
310 +
311 + writer.write("\r\n--${boundary}--\r\n")
312 + writer.flush()
313 + }
314 +
315 + // Get response
316 + def responseCode = connection.responseCode
317 + if (responseCode == 201) {
318 + def deploymentId = connection.inputStream.text.trim()
319 + println " ✓ Upload successful (HTTP ${responseCode})"
320 + return deploymentId
321 + } else {
322 + def errorMessage = connection.errorStream?.text ?: "Unknown error"
323 + throw new GradleException("Upload failed (HTTP ${responseCode}): ${errorMessage}")
324 + }
325 +}
326 +
327 +def waitForValidation(String deploymentId, String username, String password, Map config) {
328 + def credentials = "${username}:${password}".bytes.encodeBase64().toString()
329 + def maxAttempts = 60 // 5 minutes with 5-second intervals
330 + def attempt = 0
331 +
332 + while (attempt < maxAttempts) {
333 + attempt++
334 +
335 + def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
336 + def connection = new URL(url).openConnection() as HttpURLConnection
337 + connection.setRequestMethod("POST")
338 + connection.setRequestProperty("Authorization", "Bearer ${credentials}")
339 + connection.setRequestProperty("Content-Type", "application/json")
340 +
341 + def responseCode = connection.responseCode
342 + if (responseCode == 200) {
343 + def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
344 + def state = response.deploymentState
345 +
346 + println " Status check ${attempt}: ${state}"
347 +
348 + switch (state) {
349 + case "PENDING":
350 + case "VALIDATING":
351 + // Continue waiting
352 + Thread.sleep(5000)
353 + break
354 + case "VALIDATED":
355 + return [success: true, state: state, response: response]
356 + case "PUBLISHED":
357 + return [success: true, state: state, response: response]
358 + case "FAILED":
359 + def errors = response.errors ?: ["Unknown validation error"]
360 + return [success: false, error: "Validation failed: ${errors.join(', ')}", response: response]
361 + default:
362 + return [success: false, error: "Unknown deployment state: ${state}", response: response]
363 + }
364 + } else {
365 + def errorMessage = connection.errorStream?.text ?: "Unknown error"
366 + throw new GradleException("Status check failed (HTTP ${responseCode}): ${errorMessage}")
367 + }
368 + }
369 +
370 + return [success: false, error: "Timeout waiting for validation (${maxAttempts * 5} seconds)"]
371 +}
372 +
373 +def publishDeployment(String deploymentId, String username, String password, Map config) {
374 + def credentials = "${username}:${password}".bytes.encodeBase64().toString()
375 + def url = "${config.centralBaseUrl}/api/v1/publisher/deployment/${deploymentId}"
376 +
377 + println " Calling publish API..."
378 +
379 + def connection = new URL(url).openConnection() as HttpURLConnection
380 + connection.setRequestMethod("POST")
381 + connection.setRequestProperty("Authorization", "Bearer ${credentials}")
382 +
383 + def responseCode = connection.responseCode
384 + if (responseCode == 204) {
385 + println " ✓ Publish request successful (HTTP ${responseCode})"
386 +
387 + // Wait for publishing to complete
388 + println " Waiting for publishing to complete..."
389 + def result = waitForPublishing(deploymentId, username, password, config)
390 + return result
391 + } else {
392 + def errorMessage = connection.errorStream?.text ?: "Unknown error"
393 + throw new GradleException("Publish failed (HTTP ${responseCode}): ${errorMessage}")
394 + }
395 +}
396 +
397 +def waitForPublishing(String deploymentId, String username, String password, Map config) {
398 + def credentials = "${username}:${password}".bytes.encodeBase64().toString()
399 + def maxAttempts = 120 // 10 minutes with 5-second intervals
400 + def attempt = 0
401 +
402 + while (attempt < maxAttempts) {
403 + attempt++
404 +
405 + def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
406 + def connection = new URL(url).openConnection() as HttpURLConnection
407 + connection.setRequestMethod("POST")
408 + connection.setRequestProperty("Authorization", "Bearer ${credentials}")
409 + connection.setRequestProperty("Content-Type", "application/json")
410 +
411 + def responseCode = connection.responseCode
412 + if (responseCode == 200) {
413 + def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
414 + def state = response.deploymentState
415 +
416 + println " Publishing status ${attempt}: ${state}"
417 +
418 + switch (state) {
419 + case "PUBLISHING":
420 + // Continue waiting
421 + Thread.sleep(5000)
422 + break
423 + case "PUBLISHED":
424 + def purls = response.purls ?: []
425 + println " ✓ Successfully published to Maven Central!"
426 + if (purls) {
427 + println " Published artifacts:"
428 + purls.each { purl -> println " - ${purl}" }
429 + }
430 + return [success: true, state: state, response: response]
431 + case "FAILED":
432 + def errors = response.errors ?: ["Unknown publishing error"]
433 + return [success: false, error: "Publishing failed: ${errors.join(', ')}", response: response]
434 + default:
435 + return [success: false, error: "Unexpected state during publishing: ${state}", response: response]
436 + }
437 + } else {
438 + def errorMessage = connection.errorStream?.text ?: "Unknown error"
439 + throw new GradleException("Publishing status check failed (HTTP ${responseCode}): ${errorMessage}")
440 + }
441 + }
442 +
443 + return [success: false, error: "Timeout waiting for publishing (${maxAttempts * 5} seconds)"]
444 +}
......
1 // Create variables with empty default values 1 // Create variables with empty default values
2 2
3 +// Central Portal credentials
4 +ext["centralPortalUsername"] = ''
5 +ext["centralPortalPassword"] = ''
6 +
3 // keyId is the last 8 characters of the GPG key 7 // keyId is the last 8 characters of the GPG key
4 ext["signing.keyId"] = '' 8 ext["signing.keyId"] = ''
5 // password is the passphrase of the GPG key 9 // password is the passphrase of the GPG key
6 ext["signing.password"] = '' 10 ext["signing.password"] = ''
7 // key is the base64 private GPG key 11 // key is the base64 private GPG key
8 ext["signing.key"] = '' 12 ext["signing.key"] = ''
9 -// osshrUsername and ossrhPassword are the account details for MavenCentral
10 -// which we’ve chosen at the Jira registration step (Sonatype site))
11 -ext["ossrhUsername"] = ''
12 -ext["ossrhPassword"] = ''
13 -ext["sonatypeStagingProfileId"] = ''
14 13
15 File secretPropsFile = project.rootProject.file('local.properties') 14 File secretPropsFile = project.rootProject.file('local.properties')
16 if (secretPropsFile.exists()) { 15 if (secretPropsFile.exists()) {
...@@ -19,24 +18,11 @@ if (secretPropsFile.exists()) { ...@@ -19,24 +18,11 @@ if (secretPropsFile.exists()) {
19 new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } 18 new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
20 p.each { name, value -> ext[name] = value } 19 p.each { name, value -> ext[name] = value }
21 } else { 20 } else {
22 - // Use system environment variables 21 + // Use system environment variables for signing
23 - ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
24 - ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
25 - ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
26 ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') 22 ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
27 ext["signing.password"] = System.getenv('SIGNING_PASSWORD') 23 ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
28 ext["signing.key"] = System.getenv('SIGNING_KEY') 24 ext["signing.key"] = System.getenv('SIGNING_KEY')
29 -} 25 + // Central Portal credentials can also come from environment
30 - 26 + ext["centralPortalUsername"] = System.getenv('CENTRAL_PORTAL_USERNAME')
31 -// Set up Sonatype repository 27 + ext["centralPortalPassword"] = System.getenv('CENTRAL_PORTAL_PASSWORD')
32 -nexusPublishing {
33 - repositories {
34 - sonatype {
35 - stagingProfileId = sonatypeStagingProfileId
36 - username = ossrhUsername
37 - password = ossrhPassword
38 - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
39 - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
40 - }
41 - }
42 } 28 }
......
...@@ -7,22 +7,18 @@ import android.content.Context; ...@@ -7,22 +7,18 @@ import android.content.Context;
7 import android.database.Cursor; 7 import android.database.Cursor;
8 import android.database.SQLException; 8 import android.database.SQLException;
9 import android.text.TextUtils; 9 import android.text.TextUtils;
10 -import android.util.Log;
11 10
12 import androidx.annotation.Nullable; 11 import androidx.annotation.Nullable;
13 12
14 -import net.sqlcipher.DatabaseUtils; 13 +import android.database.DatabaseUtils;
15 -import net.sqlcipher.database.SQLiteDatabase; 14 +import android.database.sqlite.SQLiteDatabase;
16 -import net.sqlcipher.database.SQLiteOpenHelper; 15 +import android.database.sqlite.SQLiteOpenHelper;
17 -import net.sqlcipher.database.SQLiteStatement;
18 16
19 -import java.io.File;
20 -import java.io.FileNotFoundException;
21 -import java.io.IOException;
22 import java.util.ArrayList; 17 import java.util.ArrayList;
23 import java.util.List; 18 import java.util.List;
24 19
25 import ly.warp.sdk.utils.constants.WarpConstants; 20 import ly.warp.sdk.utils.constants.WarpConstants;
21 +import ly.warp.sdk.utils.CryptoUtils;
26 22
27 public class WarplyDBHelper extends SQLiteOpenHelper { 23 public class WarplyDBHelper extends SQLiteOpenHelper {
28 24
...@@ -30,13 +26,8 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -30,13 +26,8 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
30 // Constants 26 // Constants
31 // =========================================================== 27 // ===========================================================
32 28
33 - private enum State { 29 + private static final String DB_NAME = "warply_v2.db";
34 - DOES_NOT_EXIST, UNENCRYPTED, ENCRYPTED 30 + private static final int DB_VERSION = 1;
35 - }
36 -
37 - private static final String DB_NAME = "warply.db";
38 - private static final int DB_VERSION = 5;
39 - private static final String KEY_CIPHER = "tn#mpOl3v3Dy1pr@W";
40 31
41 //------------------------------ Fields -----------------------------// 32 //------------------------------ Fields -----------------------------//
42 private static String TABLE_REQUESTS = "requests"; 33 private static String TABLE_REQUESTS = "requests";
...@@ -110,7 +101,6 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -110,7 +101,6 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
110 101
111 public static synchronized WarplyDBHelper getInstance(Context context) { 102 public static synchronized WarplyDBHelper getInstance(Context context) {
112 if (mDBHelperInstance == null) { 103 if (mDBHelperInstance == null) {
113 - SQLiteDatabase.loadLibs(context);
114 mDBHelperInstance = new WarplyDBHelper(context); 104 mDBHelperInstance = new WarplyDBHelper(context);
115 } 105 }
116 return mDBHelperInstance; 106 return mDBHelperInstance;
...@@ -118,19 +108,15 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -118,19 +108,15 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
118 108
119 private WarplyDBHelper(Context context) { 109 private WarplyDBHelper(Context context) {
120 super(context, DB_NAME, null, DB_VERSION); 110 super(context, DB_NAME, null, DB_VERSION);
121 - State tempDatabaseState = getDatabaseState(context, DB_NAME);
122 - if (tempDatabaseState.equals(State.UNENCRYPTED)) {
123 - encrypt(context, context.getDatabasePath(DB_NAME), KEY_CIPHER.getBytes());
124 - }
125 } 111 }
126 112
127 /** 113 /**
128 - * If database connection already initialized, return the db. Else create a 114 + * Get standard SQLite database connection
129 - * new one
130 */ 115 */
131 private SQLiteDatabase getDb() { 116 private SQLiteDatabase getDb() {
132 - if (mDb == null) 117 + if (mDb == null || !mDb.isOpen()) {
133 - mDb = getWritableDatabase(KEY_CIPHER); 118 + mDb = getWritableDatabase();
119 + }
134 return mDb; 120 return mDb;
135 } 121 }
136 122
...@@ -215,13 +201,13 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -215,13 +201,13 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
215 return false; 201 return false;
216 } 202 }
217 203
218 - //------------------------------ Auth -----------------------------// 204 + //------------------------------ Auth (with field-level encryption) -----------------------------//
219 public synchronized void saveClientAccess(String clientId, String clientSecret) { 205 public synchronized void saveClientAccess(String clientId, String clientSecret) {
220 ContentValues values = new ContentValues(); 206 ContentValues values = new ContentValues();
221 if (!TextUtils.isEmpty(clientId)) 207 if (!TextUtils.isEmpty(clientId))
222 - values.put(KEY_CLIENT_ID, clientId); 208 + values.put(KEY_CLIENT_ID, CryptoUtils.encrypt(clientId));
223 if (!TextUtils.isEmpty(clientSecret)) 209 if (!TextUtils.isEmpty(clientSecret))
224 - values.put(KEY_CLIENT_SECRET, clientSecret); 210 + values.put(KEY_CLIENT_SECRET, CryptoUtils.encrypt(clientSecret));
225 if (isTableNotEmpty(TABLE_CLIENT)) 211 if (isTableNotEmpty(TABLE_CLIENT))
226 update(TABLE_CLIENT, values); 212 update(TABLE_CLIENT, values);
227 else 213 else
...@@ -231,9 +217,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -231,9 +217,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
231 public synchronized void saveAuthAccess(String accessToken, String refreshToken) { 217 public synchronized void saveAuthAccess(String accessToken, String refreshToken) {
232 ContentValues values = new ContentValues(); 218 ContentValues values = new ContentValues();
233 if (!TextUtils.isEmpty(accessToken)) 219 if (!TextUtils.isEmpty(accessToken))
234 - values.put(KEY_ACCESS_TOKEN, accessToken); 220 + values.put(KEY_ACCESS_TOKEN, CryptoUtils.encrypt(accessToken));
235 if (!TextUtils.isEmpty(refreshToken)) 221 if (!TextUtils.isEmpty(refreshToken))
236 - values.put(KEY_REFRESH_TOKEN, refreshToken); 222 + values.put(KEY_REFRESH_TOKEN, CryptoUtils.encrypt(refreshToken));
237 if (isTableNotEmpty(TABLE_AUTH)) 223 if (isTableNotEmpty(TABLE_AUTH))
238 update(TABLE_AUTH, values); 224 update(TABLE_AUTH, values);
239 else 225 else
...@@ -244,9 +230,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -244,9 +230,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
244 public synchronized String getAuthValue(String columnName) { 230 public synchronized String getAuthValue(String columnName) {
245 String columnValue = ""; 231 String columnValue = "";
246 Cursor cursor = getDb().query(TABLE_AUTH, new String[]{columnName}, null, null, null, null, null); 232 Cursor cursor = getDb().query(TABLE_AUTH, new String[]{columnName}, null, null, null, null, null);
247 - if (cursor != null) { 233 + if (cursor != null && cursor.moveToFirst()) {
248 - cursor.moveToFirst(); 234 + String encryptedValue = cursor.getString(cursor.getColumnIndex(columnName));
249 - columnValue = cursor.getString(cursor.getColumnIndex(columnName)); 235 + columnValue = CryptoUtils.decrypt(encryptedValue);
250 cursor.close(); 236 cursor.close();
251 } 237 }
252 return columnValue; 238 return columnValue;
...@@ -256,9 +242,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -256,9 +242,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
256 public synchronized String getClientValue(String columnName) { 242 public synchronized String getClientValue(String columnName) {
257 String columnValue = ""; 243 String columnValue = "";
258 Cursor cursor = getDb().query(TABLE_CLIENT, new String[]{columnName}, null, null, null, null, null); 244 Cursor cursor = getDb().query(TABLE_CLIENT, new String[]{columnName}, null, null, null, null, null);
259 - if (cursor != null) { 245 + if (cursor != null && cursor.moveToFirst()) {
260 - cursor.moveToFirst(); 246 + String encryptedValue = cursor.getString(cursor.getColumnIndex(columnName));
261 - columnValue = cursor.getString(cursor.getColumnIndex(columnName)); 247 + columnValue = CryptoUtils.decrypt(encryptedValue);
262 cursor.close(); 248 cursor.close();
263 } 249 }
264 return columnValue; 250 return columnValue;
...@@ -343,15 +329,15 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -343,15 +329,15 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
343 } 329 }
344 330
345 public synchronized long getRequestsInQueueCount() { 331 public synchronized long getRequestsInQueueCount() {
346 - return DatabaseUtils.queryNumEntries(getReadableDatabase(KEY_CIPHER), TABLE_REQUESTS); 332 + return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_REQUESTS);
347 } 333 }
348 334
349 public synchronized long getPushRequestsInQueueCount() { 335 public synchronized long getPushRequestsInQueueCount() {
350 - return DatabaseUtils.queryNumEntries(getReadableDatabase(KEY_CIPHER), TABLE_PUSH_REQUESTS); 336 + return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_PUSH_REQUESTS);
351 } 337 }
352 338
353 public synchronized long getPushAckRequestsInQueueCount() { 339 public synchronized long getPushAckRequestsInQueueCount() {
354 - return DatabaseUtils.queryNumEntries(getReadableDatabase(KEY_CIPHER), TABLE_PUSH_ACK_REQUESTS); 340 + return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_PUSH_ACK_REQUESTS);
355 } 341 }
356 342
357 public synchronized void deleteRequests(Long... ids) { 343 public synchronized void deleteRequests(Long... ids) {
...@@ -483,73 +469,11 @@ public class WarplyDBHelper extends SQLiteOpenHelper { ...@@ -483,73 +469,11 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
483 469
484 tags = new ArrayList<>(cursor.getCount()); 470 tags = new ArrayList<>(cursor.getCount());
485 while (cursor.moveToNext()) { 471 while (cursor.moveToNext()) {
486 - tags.add(cursor.getString(cursor 472 + tags.add(cursor.getString(cursor.getColumnIndex(KEY_TAG)));
487 - .getColumnIndex(KEY_TAG)));
488 } 473 }
489 cursor.close(); 474 cursor.close();
490 } 475 }
491 return tags != null ? tags.toArray(new String[tags.size()]) : null; 476 return tags != null ? tags.toArray(new String[tags.size()]) : null;
492 } 477 }
493 478
494 - private State getDatabaseState(Context context, String dbName) {
495 - SQLiteDatabase.loadLibs(context);
496 -
497 - return (getDatabaseState(context.getDatabasePath(dbName)));
498 - }
499 -
500 - private static State getDatabaseState(File dbPath) {
501 - if (dbPath.exists()) {
502 - SQLiteDatabase db = null;
503 - try {
504 - db = SQLiteDatabase.openDatabase(dbPath.getAbsolutePath(), "", null, SQLiteDatabase.OPEN_READONLY);
505 - db.getVersion();
506 -
507 - return (State.UNENCRYPTED);
508 - } catch (Exception e) {
509 - return (State.ENCRYPTED);
510 - } finally {
511 - if (db != null) {
512 - db.close();
513 - }
514 - }
515 - }
516 -
517 - return (State.DOES_NOT_EXIST);
518 - }
519 -
520 - private void encrypt(Context context, File originalFile, byte[] passphrase) {
521 - SQLiteDatabase.loadLibs(context);
522 -
523 - try {
524 - if (originalFile.exists()) {
525 - File newFile = File.createTempFile("sqlcipherutils", "tmp", context.getCacheDir());
526 - SQLiteDatabase db = SQLiteDatabase.openDatabase(originalFile.getAbsolutePath(),
527 - "", null, SQLiteDatabase.OPEN_READWRITE);
528 - int version = db.getVersion();
529 -
530 - db.close();
531 -
532 - db = SQLiteDatabase.openDatabase(newFile.getAbsolutePath(), passphrase,
533 - null, SQLiteDatabase.OPEN_READWRITE, null, null);
534 -
535 - final SQLiteStatement st = db.compileStatement("ATTACH DATABASE ? AS plaintext KEY ''");
536 -
537 - st.bindString(1, originalFile.getAbsolutePath());
538 - st.execute();
539 -
540 - db.rawExecSQL("SELECT sqlcipher_export('main', 'plaintext')");
541 - db.rawExecSQL("DETACH DATABASE plaintext");
542 - db.setVersion(version);
543 - st.close();
544 - db.close();
545 -
546 - originalFile.delete();
547 - newFile.renameTo(originalFile);
548 - } else {
549 - throw new FileNotFoundException(originalFile.getAbsolutePath() + " not found");
550 - }
551 - } catch (IOException ex) {
552 - Log.v("WarplyDB Exception: ", ex.getMessage());
553 - }
554 - }
555 } 479 }
......
1 +package ly.warp.sdk.utils;
2 +
3 +import android.security.keystore.KeyGenParameterSpec;
4 +import android.security.keystore.KeyProperties;
5 +import android.util.Base64;
6 +import android.util.Log;
7 +
8 +import java.security.KeyStore;
9 +
10 +import javax.crypto.Cipher;
11 +import javax.crypto.KeyGenerator;
12 +import javax.crypto.SecretKey;
13 +import javax.crypto.spec.GCMParameterSpec;
14 +
15 +/**
16 + * Utility class for encrypting and decrypting sensitive data using Android Keystore
17 + */
18 +public class CryptoUtils {
19 +
20 + private static final String TAG = "CryptoUtils";
21 + private static final String TRANSFORMATION = "AES/GCM/NoPadding";
22 + private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
23 + private static final String KEY_ALIAS = "tn#mpOl3v3Dy1pr@W";
24 + private static final int GCM_IV_LENGTH = 12;
25 + private static final int GCM_TAG_LENGTH = 16;
26 +
27 + /**
28 + * Encrypt a plain text string
29 + *
30 + * @param plainText The text to encrypt
31 + * @return Base64 encoded encrypted string, or original text if encryption fails
32 + */
33 + public static String encrypt(String plainText) {
34 + if (plainText == null || plainText.isEmpty()) {
35 + return plainText;
36 + }
37 +
38 + try {
39 + SecretKey secretKey = getOrCreateSecretKey();
40 + Cipher cipher = Cipher.getInstance(TRANSFORMATION);
41 + cipher.init(Cipher.ENCRYPT_MODE, secretKey);
42 +
43 + byte[] iv = cipher.getIV();
44 + byte[] encryptedData = cipher.doFinal(plainText.getBytes("UTF-8"));
45 +
46 + // Combine IV + encrypted data
47 + byte[] combined = new byte[iv.length + encryptedData.length];
48 + System.arraycopy(iv, 0, combined, 0, iv.length);
49 + System.arraycopy(encryptedData, 0, combined, iv.length, encryptedData.length);
50 +
51 + return Base64.encodeToString(combined, Base64.DEFAULT);
52 + } catch (Exception e) {
53 + Log.e(TAG, "Encryption failed, returning original text", e);
54 + // Return original text if encryption fails - graceful degradation
55 + return plainText;
56 + }
57 + }
58 +
59 + /**
60 + * Decrypt an encrypted string
61 + *
62 + * @param encryptedText The Base64 encoded encrypted text
63 + * @return Decrypted plain text, or original text if decryption fails
64 + */
65 + public static String decrypt(String encryptedText) {
66 + if (encryptedText == null || encryptedText.isEmpty()) {
67 + return encryptedText;
68 + }
69 +
70 + try {
71 + SecretKey secretKey = getOrCreateSecretKey();
72 + byte[] combined = Base64.decode(encryptedText, Base64.DEFAULT);
73 +
74 + // Extract IV and encrypted data
75 + byte[] iv = new byte[GCM_IV_LENGTH];
76 + byte[] encryptedData = new byte[combined.length - GCM_IV_LENGTH];
77 + System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH);
78 + System.arraycopy(combined, GCM_IV_LENGTH, encryptedData, 0, encryptedData.length);
79 +
80 + Cipher cipher = Cipher.getInstance(TRANSFORMATION);
81 + GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
82 + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
83 +
84 + byte[] decryptedData = cipher.doFinal(encryptedData);
85 + return new String(decryptedData, "UTF-8");
86 + } catch (Exception e) {
87 + Log.e(TAG, "Decryption failed, returning original text", e);
88 + // Return original text if decryption fails - might be unencrypted legacy data
89 + return encryptedText;
90 + }
91 + }
92 +
93 + /**
94 + * Get or create the secret key in Android Keystore
95 + */
96 + private static SecretKey getOrCreateSecretKey() throws Exception {
97 + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
98 + keyStore.load(null);
99 +
100 + if (!keyStore.containsAlias(KEY_ALIAS)) {
101 + KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
102 + KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS,
103 + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
104 + .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
105 + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
106 + .setRandomizedEncryptionRequired(true)
107 + .build();
108 + keyGenerator.init(keyGenParameterSpec);
109 + keyGenerator.generateKey();
110 + }
111 +
112 + return ((KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null)).getSecretKey();
113 + }
114 +
115 + /**
116 + * Check if a string appears to be encrypted (Base64 format)
117 + *
118 + * @param text The text to check
119 + * @return true if text appears to be encrypted
120 + */
121 + public static boolean isEncrypted(String text) {
122 + if (text == null || text.isEmpty()) {
123 + return false;
124 + }
125 +
126 + try {
127 + byte[] decoded = Base64.decode(text, Base64.DEFAULT);
128 + // Check if decoded length is reasonable for encrypted data (IV + data + tag)
129 + return decoded.length > GCM_IV_LENGTH + GCM_TAG_LENGTH;
130 + } catch (Exception e) {
131 + return false;
132 + }
133 + }
134 +}
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<resources>
3 + <attr name="colorPrimary" format="color" />
4 +</resources>
...\ No newline at end of file ...\ No newline at end of file