Showing
12 changed files
with
554 additions
and
141 deletions
| 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' |
| ... | @@ -80,4 +79,366 @@ signing { | ... | @@ -80,4 +79,366 @@ signing { |
| 80 | rootProject.ext["signing.password"], | 79 | rootProject.ext["signing.password"], |
| 81 | ) | 80 | ) |
| 82 | sign publishing.publications | 81 | sign publishing.publications |
| 83 | -} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 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') |
| 25 | + // Central Portal credentials can also come from environment | ||
| 26 | + ext["centralPortalUsername"] = System.getenv('CENTRAL_PORTAL_USERNAME') | ||
| 27 | + ext["centralPortalPassword"] = System.getenv('CENTRAL_PORTAL_PASSWORD') | ||
| 29 | } | 28 | } |
| 30 | - | ||
| 31 | -// Set up Sonatype repository | ||
| 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 | -} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -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 | +} |
-
Please register or login to post a comment