Toggle navigation
Toggle navigation
This project
Loading...
Sign in
open-source
/
warply_android_sdk_maven_plugin
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Snippets
Network
Create a new issue
Builds
Commits
Issue Boards
Authored by
Panagiotis Triantafyllou
2025-07-03 19:15:07 +0300
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
63614b65bc467cf75938f7d7b409ec12c272561e
63614b65
1 parent
2186114c
migrate to maven central, db fixes for android 15
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
553 additions
and
140 deletions
.idea/compiler.xml
.idea/gradle.xml
.idea/misc.xml
app/build.gradle
app/src/main/assets/warply.properties
build.gradle
gradle/wrapper/gradle-wrapper.properties
scripts/publish-module.gradle
scripts/publish-root.gradle
warply_android_sdk/src/main/java/ly/warp/sdk/db/WarplyDBHelper.java
warply_android_sdk/src/main/java/ly/warp/sdk/utils/CryptoUtils.java
warply_android_sdk/src/main/res/values/attrs.xml
.idea/compiler.xml
View file @
63614b6
<?xml version="1.0" encoding="UTF-8"?>
<project
version=
"4"
>
<component
name=
"CompilerConfiguration"
>
<bytecodeTargetLevel
target=
"
17
"
/>
<bytecodeTargetLevel
target=
"
21
"
/>
</component>
</project>
\ No newline at end of file
...
...
.idea/gradle.xml
View file @
63614b6
...
...
@@ -3,6 +3,7 @@
<component
name=
"GradleSettings"
>
<option
name=
"linkedExternalProjectsSettings"
>
<GradleProjectSettings>
<option
name=
"testRunner"
value=
"CHOOSE_PER_TEST"
/>
<option
name=
"externalProjectPath"
value=
"$PROJECT_DIR$"
/>
<option
name=
"gradleJvm"
value=
"jbr-17"
/>
<option
name=
"modules"
>
...
...
.idea/misc.xml
View file @
63614b6
<?xml version="1.0" encoding="UTF-8"?>
<project
version=
"4"
>
<component
name=
"ExternalStorageConfigurationManager"
enabled=
"true"
/>
<component
name=
"ProjectRootManager"
version=
"2"
languageLevel=
"JDK_
17"
default=
"true"
project-jdk-name=
"jbr-17
"
project-jdk-type=
"JavaSDK"
>
<component
name=
"ProjectRootManager"
version=
"2"
languageLevel=
"JDK_
21"
default=
"true"
project-jdk-name=
"jbr-21
"
project-jdk-type=
"JavaSDK"
>
<output
url=
"file://$PROJECT_DIR$/build/classes"
/>
</component>
</project>
\ No newline at end of file
...
...
app/build.gradle
View file @
63614b6
...
...
@@ -2,13 +2,14 @@ apply plugin: 'com.android.application'
apply
plugin:
'com.google.gms.google-services'
android
{
compileSdkVersion
3
3
buildToolsVersion
"3
3.0.2
"
compileSdkVersion
3
5
buildToolsVersion
"3
5.0.0
"
namespace
"warp.ly.android_sdk"
defaultConfig
{
applicationId
"warp.ly.android_sdk"
minSdkVersion
23
targetSdkVersion
3
3
targetSdkVersion
3
5
versionCode
100
versionName
"1.0.0"
}
...
...
app/src/main/assets/warply.properties
View file @
63614b6
...
...
@@ -4,7 +4,7 @@
Uuid
=
b13ade8ef743468b89a7aaa8efbfc468
# If we need to see logs in Logcat
Debug
=
fals
e
Debug
=
tru
e
# Production or Development environment of the engage server
# Production: https://engage.warp.ly
...
...
build.gradle
View file @
63614b6
...
...
@@ -8,16 +8,19 @@ buildscript {
maven
{
url
'https://plugins.gradle.org/m2/'
}
}
dependencies
{
classpath
'com.android.tools.build:gradle:7.0.4'
classpath
'com.google.gms:google-services:4.3.15'
classpath
'com.huawei.agconnect:agcp:1.7.2.300'
classpath
'io.github.gradle-nexus:publish-plugin:1.1.0'
classpath
'com.android.tools.build:gradle:8.7.3'
classpath
'com.google.gms:google-services:4.4.3'
classpath
'com.huawei.agconnect:agcp:1.9.3.300'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
plugins
{
id
'maven-publish'
}
allprojects
{
repositories
{
mavenCentral
()
...
...
@@ -27,5 +30,4 @@ allprojects {
}
}
apply
plugin:
'io.github.gradle-nexus.publish-plugin'
apply
from:
"${rootDir}/scripts/publish-root.gradle"
\ No newline at end of file
...
...
gradle/wrapper/gradle-wrapper.properties
View file @
63614b6
distributionBase
=
GRADLE_USER_HOME
distributionPath
=
wrapper/dists
distributionUrl
=
https
\:
//services.gradle.org/distributions/gradle-
7.2-bin
.zip
distributionUrl
=
https
\:
//services.gradle.org/distributions/gradle-
8.12-all
.zip
zipStoreBase
=
GRADLE_USER_HOME
zipStorePath
=
wrapper/dists
...
...
scripts/publish-module.gradle
View file @
63614b6
...
...
@@ -32,15 +32,15 @@ afterEvaluate {
// Two artifacts, the `aar` (or `jar`) and the sources
if
(
project
.
plugins
.
findPlugin
(
"com.android.library"
))
{
from
components
.
release
from
(
project
.
components
.
findByName
(
"release"
))
}
else
{
from
components
.
java
}
artifact
androidSourcesJar
// Sources are now handled by the android block's singleVariant
// artifact javadocJar
//
Mostly self-explanatory metadata
//
POM metadata for Maven Central
pom
{
name
=
PUBLISH_ARTIFACT_ID
description
=
'Warply Android SDK Maven Plugin'
...
...
@@ -60,8 +60,7 @@ afterEvaluate {
// Add all other devs here...
}
// Version control info - if you're using GitHub, follow the
// format as seen here
// Version control info
scm
{
connection
=
'scm:git:git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
developerConnection
=
'scm:git:ssh://git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
...
...
@@ -81,3 +80,365 @@ signing {
)
sign
publishing
.
publications
}
// Configuration for Central Publishing (similar to Maven plugin configuration)
ext
.
centralPublishing
=
[
autoPublish
:
false
,
// Manual publishing for safety
// waitUntil: "published", // Commented out - don't wait for publishing
deploymentName:
"Warply Android SDK ${PUBLISH_VERSION}"
,
centralBaseUrl:
"https://central.sonatype.com"
]
// Custom task that implements the same functionality as org.sonatype.central:central-publishing-maven-plugin:0.8.0
// This uses the Central Portal API directly to achieve the same result
task
publishToCentralPortal
{
dependsOn
'publishReleasePublicationToMavenLocal'
description
=
'Publishes to Maven Central Portal using the same API as central-publishing-maven-plugin:0.8.0'
group
=
'publishing'
doLast
{
def
username
=
rootProject
.
ext
[
"centralPortalUsername"
]
def
password
=
rootProject
.
ext
[
"centralPortalPassword"
]
def
config
=
project
.
ext
.
centralPublishing
if
(!
username
||
!
password
)
{
throw
new
GradleException
(
"Central Portal credentials not configured. Please set centralPortalUsername and centralPortalPassword in local.properties or environment variables."
)
}
println
"=== Central Portal Publishing ==="
println
"Deployment: ${config.deploymentName}"
println
"Auto-publish: ${config.autoPublish}"
println
"Portal URL: ${config.centralBaseUrl}"
println
""
// Step 1: Create deployment bundle
println
"Step 1: Creating deployment bundle..."
def
bundleFile
=
createDeploymentBundle
()
println
"✓ Bundle created: ${bundleFile.name} (${bundleFile.length()} bytes)"
// Step 2: Upload bundle to Central Portal
println
"\nStep 2: Uploading to Central Portal..."
def
deploymentId
=
uploadBundle
(
bundleFile
,
username
,
password
,
config
)
println
"✓ Upload successful. Deployment ID: ${deploymentId}"
// Step 3: Wait for validation
println
"\nStep 3: Waiting for validation..."
def
validationResult
=
waitForValidation
(
deploymentId
,
username
,
password
,
config
)
if
(
validationResult
.
success
)
{
def
state
=
validationResult
.
state
println
"✓ Validation successful! State: ${state}"
if
(
config
.
autoPublish
&&
state
==
"VALIDATED"
)
{
println
"\nStep 4: Auto-publishing..."
def
publishResult
=
publishDeployment
(
deploymentId
,
username
,
password
,
config
)
if
(
publishResult
.
success
)
{
println
"✓ Published successfully to Maven Central!"
}
else
{
throw
new
GradleException
(
"Auto-publishing failed: ${publishResult.error}"
)
}
}
else
if
(
state
==
"PUBLISHED"
)
{
println
"✓ Already published to Maven Central!"
def
response
=
validationResult
.
response
def
purls
=
response
.
purls
?:
[]
if
(
purls
)
{
println
" Published artifacts:"
purls
.
each
{
purl
->
println
" - ${purl}"
}
}
}
else
{
println
"\n✓ Deployment uploaded and validated successfully!"
println
"📋 Manual action required:"
println
" Visit: ${config.centralBaseUrl}/publishing/deployments"
println
" Find deployment: ${config.deploymentName}"
println
" Click 'Publish' to complete the process"
}
}
else
{
throw
new
GradleException
(
"Validation failed: ${validationResult.error}"
)
}
println
"\n=== Publishing Complete ==="
}
}
def
createDeploymentBundle
()
{
def
bundleDir
=
file
(
"${buildDir}/central-publishing"
)
def
stagingDir
=
file
(
"${bundleDir}/staging"
)
// Clean and create directories
bundleDir
.
deleteDir
()
stagingDir
.
mkdirs
()
// Create Maven repository structure
def
groupPath
=
PUBLISH_GROUP_ID
.
replace
(
'.'
,
'/'
)
def
artifactDir
=
file
(
"${stagingDir}/${groupPath}/${PUBLISH_ARTIFACT_ID}/${PUBLISH_VERSION}"
)
artifactDir
.
mkdirs
()
// Copy artifacts to staging area
def
artifacts
=
[:]
// AAR file
def
aarFile
=
file
(
"${buildDir}/outputs/aar/warply_android_sdk-release.aar"
)
if
(
aarFile
.
exists
())
{
def
targetAar
=
file
(
"${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.aar"
)
copy
{
from
aarFile
into
artifactDir
rename
{
targetAar
.
name
}
}
artifacts
[
'aar'
]
=
targetAar
// Copy AAR signature if exists
def
aarSigFile
=
file
(
"${aarFile.path}.asc"
)
if
(
aarSigFile
.
exists
())
{
def
targetAarSig
=
file
(
"${targetAar.path}.asc"
)
copy
{
from
aarSigFile
into
artifactDir
rename
{
targetAarSig
.
name
}
}
artifacts
[
'aar-sig'
]
=
targetAarSig
}
}
// Sources JAR
def
sourcesFile
=
file
(
"${buildDir}/libs/warply_android_sdk-${PUBLISH_VERSION}-sources.jar"
)
if
(
sourcesFile
.
exists
())
{
def
targetSources
=
file
(
"${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}-sources.jar"
)
copy
{
from
sourcesFile
into
artifactDir
rename
{
targetSources
.
name
}
}
artifacts
[
'sources'
]
=
targetSources
// Copy sources signature if exists
def
sourcesSigFile
=
file
(
"${sourcesFile.path}.asc"
)
if
(
sourcesSigFile
.
exists
())
{
def
targetSourcesSig
=
file
(
"${targetSources.path}.asc"
)
copy
{
from
sourcesSigFile
into
artifactDir
rename
{
targetSourcesSig
.
name
}
}
artifacts
[
'sources-sig'
]
=
targetSourcesSig
}
}
// POM file
def
pomFile
=
file
(
"${buildDir}/publications/release/pom-default.xml"
)
if
(
pomFile
.
exists
())
{
def
targetPom
=
file
(
"${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.pom"
)
copy
{
from
pomFile
into
artifactDir
rename
{
targetPom
.
name
}
}
artifacts
[
'pom'
]
=
targetPom
// Copy POM signature if exists
def
pomSigFile
=
file
(
"${pomFile.path}.asc"
)
if
(
pomSigFile
.
exists
())
{
def
targetPomSig
=
file
(
"${targetPom.path}.asc"
)
copy
{
from
pomSigFile
into
artifactDir
rename
{
targetPomSig
.
name
}
}
artifacts
[
'pom-sig'
]
=
targetPomSig
}
}
// Generate checksums for all files
artifacts
.
each
{
type
,
artifactFile
->
if
(
artifactFile
.
exists
())
{
generateChecksums
(
artifactFile
)
}
}
// Create bundle ZIP
def
bundleFile
=
file
(
"${bundleDir}/central-bundle.zip"
)
ant
.
zip
(
destfile:
bundleFile
)
{
fileset
(
dir:
stagingDir
)
}
return
bundleFile
}
def
generateChecksums
(
File
file
)
{
[
'md5'
,
'sha1'
,
'sha256'
,
'sha512'
].
each
{
algorithm
->
def
checksum
=
file
.
withInputStream
{
stream
->
java
.
security
.
MessageDigest
.
getInstance
(
algorithm
.
toUpperCase
()).
digest
(
stream
.
bytes
).
encodeHex
().
toString
()
}
new
File
(
"${file.path}.${algorithm}"
).
text
=
checksum
}
}
def
uploadBundle
(
File
bundleFile
,
String
username
,
String
password
,
Map
config
)
{
def
url
=
"${config.centralBaseUrl}/api/v1/publisher/upload"
def
credentials
=
"${username}:${password}"
.
bytes
.
encodeBase64
().
toString
()
// Add query parameters
def
publishingType
=
config
.
autoPublish
?
"AUTOMATIC"
:
"USER_MANAGED"
def
urlWithParams
=
"${url}?publishingType=${publishingType}&name=${URLEncoder.encode(config.deploymentName, 'UTF-8')}"
println
" Uploading to: ${urlWithParams}"
println
" Publishing type: ${publishingType}"
def
connection
=
new
URL
(
urlWithParams
).
openConnection
()
as
HttpURLConnection
connection
.
setRequestMethod
(
"POST"
)
connection
.
setRequestProperty
(
"Authorization"
,
"Bearer ${credentials}"
)
connection
.
setDoOutput
(
true
)
connection
.
setDoInput
(
true
)
// Create multipart/form-data boundary
def
boundary
=
"----WebKitFormBoundary"
+
System
.
currentTimeMillis
()
connection
.
setRequestProperty
(
"Content-Type"
,
"multipart/form-data; boundary=${boundary}"
)
// Write multipart data
connection
.
outputStream
.
withWriter
(
"UTF-8"
)
{
writer
->
writer
.
write
(
"--${boundary}\r\n"
)
writer
.
write
(
"Content-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.name}\"\r\n"
)
writer
.
write
(
"Content-Type: application/octet-stream\r\n"
)
writer
.
write
(
"\r\n"
)
writer
.
flush
()
// Write file content
bundleFile
.
withInputStream
{
input
->
connection
.
outputStream
<<
input
}
writer
.
write
(
"\r\n--${boundary}--\r\n"
)
writer
.
flush
()
}
// Get response
def
responseCode
=
connection
.
responseCode
if
(
responseCode
==
201
)
{
def
deploymentId
=
connection
.
inputStream
.
text
.
trim
()
println
" ✓ Upload successful (HTTP ${responseCode})"
return
deploymentId
}
else
{
def
errorMessage
=
connection
.
errorStream
?.
text
?:
"Unknown error"
throw
new
GradleException
(
"Upload failed (HTTP ${responseCode}): ${errorMessage}"
)
}
}
def
waitForValidation
(
String
deploymentId
,
String
username
,
String
password
,
Map
config
)
{
def
credentials
=
"${username}:${password}"
.
bytes
.
encodeBase64
().
toString
()
def
maxAttempts
=
60
// 5 minutes with 5-second intervals
def
attempt
=
0
while
(
attempt
<
maxAttempts
)
{
attempt
++
def
url
=
"${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
def
connection
=
new
URL
(
url
).
openConnection
()
as
HttpURLConnection
connection
.
setRequestMethod
(
"POST"
)
connection
.
setRequestProperty
(
"Authorization"
,
"Bearer ${credentials}"
)
connection
.
setRequestProperty
(
"Content-Type"
,
"application/json"
)
def
responseCode
=
connection
.
responseCode
if
(
responseCode
==
200
)
{
def
response
=
new
groovy
.
json
.
JsonSlurper
().
parseText
(
connection
.
inputStream
.
text
)
def
state
=
response
.
deploymentState
println
" Status check ${attempt}: ${state}"
switch
(
state
)
{
case
"PENDING"
:
case
"VALIDATING"
:
// Continue waiting
Thread
.
sleep
(
5000
)
break
case
"VALIDATED"
:
return
[
success:
true
,
state:
state
,
response:
response
]
case
"PUBLISHED"
:
return
[
success:
true
,
state:
state
,
response:
response
]
case
"FAILED"
:
def
errors
=
response
.
errors
?:
[
"Unknown validation error"
]
return
[
success:
false
,
error:
"Validation failed: ${errors.join(', ')}"
,
response:
response
]
default:
return
[
success:
false
,
error:
"Unknown deployment state: ${state}"
,
response:
response
]
}
}
else
{
def
errorMessage
=
connection
.
errorStream
?.
text
?:
"Unknown error"
throw
new
GradleException
(
"Status check failed (HTTP ${responseCode}): ${errorMessage}"
)
}
}
return
[
success:
false
,
error:
"Timeout waiting for validation (${maxAttempts * 5} seconds)"
]
}
def
publishDeployment
(
String
deploymentId
,
String
username
,
String
password
,
Map
config
)
{
def
credentials
=
"${username}:${password}"
.
bytes
.
encodeBase64
().
toString
()
def
url
=
"${config.centralBaseUrl}/api/v1/publisher/deployment/${deploymentId}"
println
" Calling publish API..."
def
connection
=
new
URL
(
url
).
openConnection
()
as
HttpURLConnection
connection
.
setRequestMethod
(
"POST"
)
connection
.
setRequestProperty
(
"Authorization"
,
"Bearer ${credentials}"
)
def
responseCode
=
connection
.
responseCode
if
(
responseCode
==
204
)
{
println
" ✓ Publish request successful (HTTP ${responseCode})"
// Wait for publishing to complete
println
" Waiting for publishing to complete..."
def
result
=
waitForPublishing
(
deploymentId
,
username
,
password
,
config
)
return
result
}
else
{
def
errorMessage
=
connection
.
errorStream
?.
text
?:
"Unknown error"
throw
new
GradleException
(
"Publish failed (HTTP ${responseCode}): ${errorMessage}"
)
}
}
def
waitForPublishing
(
String
deploymentId
,
String
username
,
String
password
,
Map
config
)
{
def
credentials
=
"${username}:${password}"
.
bytes
.
encodeBase64
().
toString
()
def
maxAttempts
=
120
// 10 minutes with 5-second intervals
def
attempt
=
0
while
(
attempt
<
maxAttempts
)
{
attempt
++
def
url
=
"${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
def
connection
=
new
URL
(
url
).
openConnection
()
as
HttpURLConnection
connection
.
setRequestMethod
(
"POST"
)
connection
.
setRequestProperty
(
"Authorization"
,
"Bearer ${credentials}"
)
connection
.
setRequestProperty
(
"Content-Type"
,
"application/json"
)
def
responseCode
=
connection
.
responseCode
if
(
responseCode
==
200
)
{
def
response
=
new
groovy
.
json
.
JsonSlurper
().
parseText
(
connection
.
inputStream
.
text
)
def
state
=
response
.
deploymentState
println
" Publishing status ${attempt}: ${state}"
switch
(
state
)
{
case
"PUBLISHING"
:
// Continue waiting
Thread
.
sleep
(
5000
)
break
case
"PUBLISHED"
:
def
purls
=
response
.
purls
?:
[]
println
" ✓ Successfully published to Maven Central!"
if
(
purls
)
{
println
" Published artifacts:"
purls
.
each
{
purl
->
println
" - ${purl}"
}
}
return
[
success:
true
,
state:
state
,
response:
response
]
case
"FAILED"
:
def
errors
=
response
.
errors
?:
[
"Unknown publishing error"
]
return
[
success:
false
,
error:
"Publishing failed: ${errors.join(', ')}"
,
response:
response
]
default:
return
[
success:
false
,
error:
"Unexpected state during publishing: ${state}"
,
response:
response
]
}
}
else
{
def
errorMessage
=
connection
.
errorStream
?.
text
?:
"Unknown error"
throw
new
GradleException
(
"Publishing status check failed (HTTP ${responseCode}): ${errorMessage}"
)
}
}
return
[
success:
false
,
error:
"Timeout waiting for publishing (${maxAttempts * 5} seconds)"
]
}
...
...
scripts/publish-root.gradle
View file @
63614b6
// Create variables with empty default values
// Central Portal credentials
ext
[
"centralPortalUsername"
]
=
''
ext
[
"centralPortalPassword"
]
=
''
// keyId is the last 8 characters of the GPG key
ext
[
"signing.keyId"
]
=
''
// password is the passphrase of the GPG key
ext
[
"signing.password"
]
=
''
// key is the base64 private GPG key
ext
[
"signing.key"
]
=
''
// osshrUsername and ossrhPassword are the account details for MavenCentral
// which we’ve chosen at the Jira registration step (Sonatype site))
ext
[
"ossrhUsername"
]
=
''
ext
[
"ossrhPassword"
]
=
''
ext
[
"sonatypeStagingProfileId"
]
=
''
File
secretPropsFile
=
project
.
rootProject
.
file
(
'local.properties'
)
if
(
secretPropsFile
.
exists
())
{
...
...
@@ -19,24 +18,11 @@ if (secretPropsFile.exists()) {
new
FileInputStream
(
secretPropsFile
).
withCloseable
{
is
->
p
.
load
(
is
)
}
p
.
each
{
name
,
value
->
ext
[
name
]
=
value
}
}
else
{
// Use system environment variables
ext
[
"ossrhUsername"
]
=
System
.
getenv
(
'OSSRH_USERNAME'
)
ext
[
"ossrhPassword"
]
=
System
.
getenv
(
'OSSRH_PASSWORD'
)
ext
[
"sonatypeStagingProfileId"
]
=
System
.
getenv
(
'SONATYPE_STAGING_PROFILE_ID'
)
// Use system environment variables for signing
ext
[
"signing.keyId"
]
=
System
.
getenv
(
'SIGNING_KEY_ID'
)
ext
[
"signing.password"
]
=
System
.
getenv
(
'SIGNING_PASSWORD'
)
ext
[
"signing.key"
]
=
System
.
getenv
(
'SIGNING_KEY'
)
}
// Set up Sonatype repository
nexusPublishing
{
repositories
{
sonatype
{
stagingProfileId
=
sonatypeStagingProfileId
username
=
ossrhUsername
password
=
ossrhPassword
nexusUrl
.
set
(
uri
(
"https://s01.oss.sonatype.org/service/local/"
))
snapshotRepositoryUrl
.
set
(
uri
(
"https://s01.oss.sonatype.org/content/repositories/snapshots/"
))
}
}
// Central Portal credentials can also come from environment
ext
[
"centralPortalUsername"
]
=
System
.
getenv
(
'CENTRAL_PORTAL_USERNAME'
)
ext
[
"centralPortalPassword"
]
=
System
.
getenv
(
'CENTRAL_PORTAL_PASSWORD'
)
}
...
...
warply_android_sdk/src/main/java/ly/warp/sdk/db/WarplyDBHelper.java
View file @
63614b6
...
...
@@ -7,22 +7,18 @@ import android.content.Context;
import
android.database.Cursor
;
import
android.database.SQLException
;
import
android.text.TextUtils
;
import
android.util.Log
;
import
androidx.annotation.Nullable
;
import
net.sqlcipher.DatabaseUtils
;
import
net.sqlcipher.database.SQLiteDatabase
;
import
net.sqlcipher.database.SQLiteOpenHelper
;
import
net.sqlcipher.database.SQLiteStatement
;
import
android.database.DatabaseUtils
;
import
android.database.sqlite.SQLiteDatabase
;
import
android.database.sqlite.SQLiteOpenHelper
;
import
java.io.File
;
import
java.io.FileNotFoundException
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
java.util.List
;
import
ly.warp.sdk.utils.constants.WarpConstants
;
import
ly.warp.sdk.utils.CryptoUtils
;
public
class
WarplyDBHelper
extends
SQLiteOpenHelper
{
...
...
@@ -30,13 +26,8 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
// Constants
// ===========================================================
private
enum
State
{
DOES_NOT_EXIST
,
UNENCRYPTED
,
ENCRYPTED
}
private
static
final
String
DB_NAME
=
"warply.db"
;
private
static
final
int
DB_VERSION
=
5
;
private
static
final
String
KEY_CIPHER
=
"tn#mpOl3v3Dy1pr@W"
;
private
static
final
String
DB_NAME
=
"warply_v2.db"
;
private
static
final
int
DB_VERSION
=
1
;
//------------------------------ Fields -----------------------------//
private
static
String
TABLE_REQUESTS
=
"requests"
;
...
...
@@ -110,7 +101,6 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public
static
synchronized
WarplyDBHelper
getInstance
(
Context
context
)
{
if
(
mDBHelperInstance
==
null
)
{
SQLiteDatabase
.
loadLibs
(
context
);
mDBHelperInstance
=
new
WarplyDBHelper
(
context
);
}
return
mDBHelperInstance
;
...
...
@@ -118,19 +108,15 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
private
WarplyDBHelper
(
Context
context
)
{
super
(
context
,
DB_NAME
,
null
,
DB_VERSION
);
State
tempDatabaseState
=
getDatabaseState
(
context
,
DB_NAME
);
if
(
tempDatabaseState
.
equals
(
State
.
UNENCRYPTED
))
{
encrypt
(
context
,
context
.
getDatabasePath
(
DB_NAME
),
KEY_CIPHER
.
getBytes
());
}
}
/**
* If database connection already initialized, return the db. Else create a
* new one
* Get standard SQLite database connection
*/
private
SQLiteDatabase
getDb
()
{
if
(
mDb
==
null
)
mDb
=
getWritableDatabase
(
KEY_CIPHER
);
if
(
mDb
==
null
||
!
mDb
.
isOpen
())
{
mDb
=
getWritableDatabase
();
}
return
mDb
;
}
...
...
@@ -215,13 +201,13 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
return
false
;
}
//------------------------------ Auth -----------------------------//
//------------------------------ Auth
(with field-level encryption)
-----------------------------//
public
synchronized
void
saveClientAccess
(
String
clientId
,
String
clientSecret
)
{
ContentValues
values
=
new
ContentValues
();
if
(!
TextUtils
.
isEmpty
(
clientId
))
values
.
put
(
KEY_CLIENT_ID
,
clientId
);
values
.
put
(
KEY_CLIENT_ID
,
CryptoUtils
.
encrypt
(
clientId
)
);
if
(!
TextUtils
.
isEmpty
(
clientSecret
))
values
.
put
(
KEY_CLIENT_SECRET
,
clientSecret
);
values
.
put
(
KEY_CLIENT_SECRET
,
CryptoUtils
.
encrypt
(
clientSecret
)
);
if
(
isTableNotEmpty
(
TABLE_CLIENT
))
update
(
TABLE_CLIENT
,
values
);
else
...
...
@@ -231,9 +217,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public
synchronized
void
saveAuthAccess
(
String
accessToken
,
String
refreshToken
)
{
ContentValues
values
=
new
ContentValues
();
if
(!
TextUtils
.
isEmpty
(
accessToken
))
values
.
put
(
KEY_ACCESS_TOKEN
,
accessToken
);
values
.
put
(
KEY_ACCESS_TOKEN
,
CryptoUtils
.
encrypt
(
accessToken
)
);
if
(!
TextUtils
.
isEmpty
(
refreshToken
))
values
.
put
(
KEY_REFRESH_TOKEN
,
refreshToken
);
values
.
put
(
KEY_REFRESH_TOKEN
,
CryptoUtils
.
encrypt
(
refreshToken
)
);
if
(
isTableNotEmpty
(
TABLE_AUTH
))
update
(
TABLE_AUTH
,
values
);
else
...
...
@@ -244,9 +230,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public
synchronized
String
getAuthValue
(
String
columnName
)
{
String
columnValue
=
""
;
Cursor
cursor
=
getDb
().
query
(
TABLE_AUTH
,
new
String
[]{
columnName
},
null
,
null
,
null
,
null
,
null
);
if
(
cursor
!=
null
)
{
cursor
.
moveToFirst
(
);
columnValue
=
cursor
.
getString
(
cursor
.
getColumnIndex
(
columnName
)
);
if
(
cursor
!=
null
&&
cursor
.
moveToFirst
()
)
{
String
encryptedValue
=
cursor
.
getString
(
cursor
.
getColumnIndex
(
columnName
)
);
columnValue
=
CryptoUtils
.
decrypt
(
encryptedValue
);
cursor
.
close
();
}
return
columnValue
;
...
...
@@ -256,9 +242,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public
synchronized
String
getClientValue
(
String
columnName
)
{
String
columnValue
=
""
;
Cursor
cursor
=
getDb
().
query
(
TABLE_CLIENT
,
new
String
[]{
columnName
},
null
,
null
,
null
,
null
,
null
);
if
(
cursor
!=
null
)
{
cursor
.
moveToFirst
(
);
columnValue
=
cursor
.
getString
(
cursor
.
getColumnIndex
(
columnName
)
);
if
(
cursor
!=
null
&&
cursor
.
moveToFirst
()
)
{
String
encryptedValue
=
cursor
.
getString
(
cursor
.
getColumnIndex
(
columnName
)
);
columnValue
=
CryptoUtils
.
decrypt
(
encryptedValue
);
cursor
.
close
();
}
return
columnValue
;
...
...
@@ -343,15 +329,15 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
public
synchronized
long
getRequestsInQueueCount
()
{
return
DatabaseUtils
.
queryNumEntries
(
getReadableDatabase
(
KEY_CIPHER
),
TABLE_REQUESTS
);
return
DatabaseUtils
.
queryNumEntries
(
getReadableDatabase
(),
TABLE_REQUESTS
);
}
public
synchronized
long
getPushRequestsInQueueCount
()
{
return
DatabaseUtils
.
queryNumEntries
(
getReadableDatabase
(
KEY_CIPHER
),
TABLE_PUSH_REQUESTS
);
return
DatabaseUtils
.
queryNumEntries
(
getReadableDatabase
(),
TABLE_PUSH_REQUESTS
);
}
public
synchronized
long
getPushAckRequestsInQueueCount
()
{
return
DatabaseUtils
.
queryNumEntries
(
getReadableDatabase
(
KEY_CIPHER
),
TABLE_PUSH_ACK_REQUESTS
);
return
DatabaseUtils
.
queryNumEntries
(
getReadableDatabase
(),
TABLE_PUSH_ACK_REQUESTS
);
}
public
synchronized
void
deleteRequests
(
Long
...
ids
)
{
...
...
@@ -483,73 +469,11 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
tags
=
new
ArrayList
<>(
cursor
.
getCount
());
while
(
cursor
.
moveToNext
())
{
tags
.
add
(
cursor
.
getString
(
cursor
.
getColumnIndex
(
KEY_TAG
)));
tags
.
add
(
cursor
.
getString
(
cursor
.
getColumnIndex
(
KEY_TAG
)));
}
cursor
.
close
();
}
return
tags
!=
null
?
tags
.
toArray
(
new
String
[
tags
.
size
()])
:
null
;
}
private
State
getDatabaseState
(
Context
context
,
String
dbName
)
{
SQLiteDatabase
.
loadLibs
(
context
);
return
(
getDatabaseState
(
context
.
getDatabasePath
(
dbName
)));
}
private
static
State
getDatabaseState
(
File
dbPath
)
{
if
(
dbPath
.
exists
())
{
SQLiteDatabase
db
=
null
;
try
{
db
=
SQLiteDatabase
.
openDatabase
(
dbPath
.
getAbsolutePath
(),
""
,
null
,
SQLiteDatabase
.
OPEN_READONLY
);
db
.
getVersion
();
return
(
State
.
UNENCRYPTED
);
}
catch
(
Exception
e
)
{
return
(
State
.
ENCRYPTED
);
}
finally
{
if
(
db
!=
null
)
{
db
.
close
();
}
}
}
return
(
State
.
DOES_NOT_EXIST
);
}
private
void
encrypt
(
Context
context
,
File
originalFile
,
byte
[]
passphrase
)
{
SQLiteDatabase
.
loadLibs
(
context
);
try
{
if
(
originalFile
.
exists
())
{
File
newFile
=
File
.
createTempFile
(
"sqlcipherutils"
,
"tmp"
,
context
.
getCacheDir
());
SQLiteDatabase
db
=
SQLiteDatabase
.
openDatabase
(
originalFile
.
getAbsolutePath
(),
""
,
null
,
SQLiteDatabase
.
OPEN_READWRITE
);
int
version
=
db
.
getVersion
();
db
.
close
();
db
=
SQLiteDatabase
.
openDatabase
(
newFile
.
getAbsolutePath
(),
passphrase
,
null
,
SQLiteDatabase
.
OPEN_READWRITE
,
null
,
null
);
final
SQLiteStatement
st
=
db
.
compileStatement
(
"ATTACH DATABASE ? AS plaintext KEY ''"
);
st
.
bindString
(
1
,
originalFile
.
getAbsolutePath
());
st
.
execute
();
db
.
rawExecSQL
(
"SELECT sqlcipher_export('main', 'plaintext')"
);
db
.
rawExecSQL
(
"DETACH DATABASE plaintext"
);
db
.
setVersion
(
version
);
st
.
close
();
db
.
close
();
originalFile
.
delete
();
newFile
.
renameTo
(
originalFile
);
}
else
{
throw
new
FileNotFoundException
(
originalFile
.
getAbsolutePath
()
+
" not found"
);
}
}
catch
(
IOException
ex
)
{
Log
.
v
(
"WarplyDB Exception: "
,
ex
.
getMessage
());
}
}
}
...
...
warply_android_sdk/src/main/java/ly/warp/sdk/utils/CryptoUtils.java
0 → 100644
View file @
63614b6
package
ly
.
warp
.
sdk
.
utils
;
import
android.security.keystore.KeyGenParameterSpec
;
import
android.security.keystore.KeyProperties
;
import
android.util.Base64
;
import
android.util.Log
;
import
java.security.KeyStore
;
import
javax.crypto.Cipher
;
import
javax.crypto.KeyGenerator
;
import
javax.crypto.SecretKey
;
import
javax.crypto.spec.GCMParameterSpec
;
/**
* Utility class for encrypting and decrypting sensitive data using Android Keystore
*/
public
class
CryptoUtils
{
private
static
final
String
TAG
=
"CryptoUtils"
;
private
static
final
String
TRANSFORMATION
=
"AES/GCM/NoPadding"
;
private
static
final
String
ANDROID_KEYSTORE
=
"AndroidKeyStore"
;
private
static
final
String
KEY_ALIAS
=
"tn#mpOl3v3Dy1pr@W"
;
private
static
final
int
GCM_IV_LENGTH
=
12
;
private
static
final
int
GCM_TAG_LENGTH
=
16
;
/**
* Encrypt a plain text string
*
* @param plainText The text to encrypt
* @return Base64 encoded encrypted string, or original text if encryption fails
*/
public
static
String
encrypt
(
String
plainText
)
{
if
(
plainText
==
null
||
plainText
.
isEmpty
())
{
return
plainText
;
}
try
{
SecretKey
secretKey
=
getOrCreateSecretKey
();
Cipher
cipher
=
Cipher
.
getInstance
(
TRANSFORMATION
);
cipher
.
init
(
Cipher
.
ENCRYPT_MODE
,
secretKey
);
byte
[]
iv
=
cipher
.
getIV
();
byte
[]
encryptedData
=
cipher
.
doFinal
(
plainText
.
getBytes
(
"UTF-8"
));
// Combine IV + encrypted data
byte
[]
combined
=
new
byte
[
iv
.
length
+
encryptedData
.
length
];
System
.
arraycopy
(
iv
,
0
,
combined
,
0
,
iv
.
length
);
System
.
arraycopy
(
encryptedData
,
0
,
combined
,
iv
.
length
,
encryptedData
.
length
);
return
Base64
.
encodeToString
(
combined
,
Base64
.
DEFAULT
);
}
catch
(
Exception
e
)
{
Log
.
e
(
TAG
,
"Encryption failed, returning original text"
,
e
);
// Return original text if encryption fails - graceful degradation
return
plainText
;
}
}
/**
* Decrypt an encrypted string
*
* @param encryptedText The Base64 encoded encrypted text
* @return Decrypted plain text, or original text if decryption fails
*/
public
static
String
decrypt
(
String
encryptedText
)
{
if
(
encryptedText
==
null
||
encryptedText
.
isEmpty
())
{
return
encryptedText
;
}
try
{
SecretKey
secretKey
=
getOrCreateSecretKey
();
byte
[]
combined
=
Base64
.
decode
(
encryptedText
,
Base64
.
DEFAULT
);
// Extract IV and encrypted data
byte
[]
iv
=
new
byte
[
GCM_IV_LENGTH
];
byte
[]
encryptedData
=
new
byte
[
combined
.
length
-
GCM_IV_LENGTH
];
System
.
arraycopy
(
combined
,
0
,
iv
,
0
,
GCM_IV_LENGTH
);
System
.
arraycopy
(
combined
,
GCM_IV_LENGTH
,
encryptedData
,
0
,
encryptedData
.
length
);
Cipher
cipher
=
Cipher
.
getInstance
(
TRANSFORMATION
);
GCMParameterSpec
spec
=
new
GCMParameterSpec
(
GCM_TAG_LENGTH
*
8
,
iv
);
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
secretKey
,
spec
);
byte
[]
decryptedData
=
cipher
.
doFinal
(
encryptedData
);
return
new
String
(
decryptedData
,
"UTF-8"
);
}
catch
(
Exception
e
)
{
Log
.
e
(
TAG
,
"Decryption failed, returning original text"
,
e
);
// Return original text if decryption fails - might be unencrypted legacy data
return
encryptedText
;
}
}
/**
* Get or create the secret key in Android Keystore
*/
private
static
SecretKey
getOrCreateSecretKey
()
throws
Exception
{
KeyStore
keyStore
=
KeyStore
.
getInstance
(
ANDROID_KEYSTORE
);
keyStore
.
load
(
null
);
if
(!
keyStore
.
containsAlias
(
KEY_ALIAS
))
{
KeyGenerator
keyGenerator
=
KeyGenerator
.
getInstance
(
KeyProperties
.
KEY_ALGORITHM_AES
,
ANDROID_KEYSTORE
);
KeyGenParameterSpec
keyGenParameterSpec
=
new
KeyGenParameterSpec
.
Builder
(
KEY_ALIAS
,
KeyProperties
.
PURPOSE_ENCRYPT
|
KeyProperties
.
PURPOSE_DECRYPT
)
.
setBlockModes
(
KeyProperties
.
BLOCK_MODE_GCM
)
.
setEncryptionPaddings
(
KeyProperties
.
ENCRYPTION_PADDING_NONE
)
.
setRandomizedEncryptionRequired
(
true
)
.
build
();
keyGenerator
.
init
(
keyGenParameterSpec
);
keyGenerator
.
generateKey
();
}
return
((
KeyStore
.
SecretKeyEntry
)
keyStore
.
getEntry
(
KEY_ALIAS
,
null
)).
getSecretKey
();
}
/**
* Check if a string appears to be encrypted (Base64 format)
*
* @param text The text to check
* @return true if text appears to be encrypted
*/
public
static
boolean
isEncrypted
(
String
text
)
{
if
(
text
==
null
||
text
.
isEmpty
())
{
return
false
;
}
try
{
byte
[]
decoded
=
Base64
.
decode
(
text
,
Base64
.
DEFAULT
);
// Check if decoded length is reasonable for encrypted data (IV + data + tag)
return
decoded
.
length
>
GCM_IV_LENGTH
+
GCM_TAG_LENGTH
;
}
catch
(
Exception
e
)
{
return
false
;
}
}
}
warply_android_sdk/src/main/res/values/attrs.xml
0 → 100644
View file @
63614b6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr
name=
"colorPrimary"
format=
"color"
/>
</resources>
\ No newline at end of file
Please
register
or
login
to post a comment